--[[ Dynamic Gameplay Camera v1.0.89 - Production Natural Fluid CLIENT-SIDE - Multi Theft Auto: San Andreas Unificação dos dois sistemas em UM controller: - Gameplay camera dinâmica enquanto dirige. - Idle cinematic automática quando o veículo fica parado. - Transição chase -> idle e idle -> chase sem setCameraTarget, evitando corte seco. - Idle cinematic agora usa 2 loops baixos/laterais contínuos, sem pontos altos de entrada. - Tecla V alterna 4 distâncias progressivas: Perto, Normal, Longe e Muito longe. - MouseOrbit real: captura o movimento do mouse com cursor invisível e converte em órbita customizada. - setCameraTarget(localPlayer) só é usado ao sair do veículo/morrer/parar resource/desativar. - v1.0.61: ao confirmar saída, dá um kick curto para a câmera do ped e devolve ao nativo, evitando ficar presa no veículo. - v1.0.59: restauração antecipada também no início da saída, inclusive pulando do veículo em movimento. - v1.0.83: Q/E em aeronaves continuam como controle nativo/roll, sem acionar câmera lateral. - v1.0.75: MouseOrbit 8D com liberdade total de ângulo enquanto dirige. - v1.0.75: adiciona roll real no setCameraMatrix para a câmera inclinar junto com o veículo e inverter ao capotar. - v1.0.77: melhora detecção de força G por aceleração longitudinal/lateral real e otimiza cache de velocidade. - v1.0.78: adiciona perfis exclusivos para aviões/helicópteros/barcos/trens e otimizações runtime. - v1.0.88: restaura fluidez natural: sem frame-skip de câmera, sem target-cache agressivo e setCameraMatrix todo frame. ]] local Config = { enabled = true, debug = false, activation = { autoStartWhenDriver = true, minSpeedForEffects = 5.0, disableOnFirstPerson = false, -- v1.0.57: segurança de assento. -- Recursos pesados (setCameraMatrix, MouseOrbit, V, Q/E, idle e emulação) só rodam no motorista. driverSeatOnly = true, restoreWhenPassenger = true, }, camera = { forwardCamOffset = { x = 0.0, y = -7.35, z = 2.28 }, forwardLookOffset = { x = 0.0, y = 2.25, z = 0.86 }, reverseCamOffset = { x = 0.0, y = 6.35, z = 2.18 }, reverseLookOffset = { x = 0.0, y = -2.65, z = 0.82 }, entryBlendMs = 950, positionSmooth = 9.5, lookSmooth = 11.0, reverseSmooth = 5.8, accelPullMax = 0.34, brakePushMax = 0.28, accelSmooth = 7.0, }, fov = { enabled = true, default = 70, max = 84, speedForMax = 220, smooth = 6.5, }, -- v1.0.77: força G refinada. -- Usa aceleração longitudinal/lateral real do veículo em relação ao próprio eixo, -- não apenas variação simples de velocidade. Isso deixa arrancada, frenagem, -- batida e curva forte mais naturais. gForce = { enabled = true, minSpeed = 6.0, deadzone = 4.0, -- Longitudinal: acelerar puxa a câmera para trás, frear/bater empurra para frente. accelPullScale = 0.0088, -- v1.0.89: menos puxada agressiva brakePushScale = 0.0076, -- v1.0.89 maxBackwardPull = 0.44, -- v1.0.89 maxForwardPush = 0.34, -- v1.0.89 -- Lateral: curva forte desloca câmera/foco com suavidade. lateralCamScale = 0.0048, -- v1.0.89 lateralLookScale = 0.0078, -- v1.0.89 maxSideOffset = 0.20, -- v1.0.89 maxLookSide = 0.35, -- v1.0.89 -- Pequeno ajuste vertical/foco para dar sensação de peso sem ficar tremido. pitchLookScale = 0.00125, -- v1.0.89 maxPitchLook = 0.10, -- v1.0.89 smooth = 10.5, resetSmooth = 8.5, }, cameraView = { enabled = true, switchKey = "v", notify = true, -- A tecla V alterna estes 4 modos externos em ordem progressiva. -- Perto -> Normal -> Longe -> Muito longe. -- A idle cinematográfica ignora esses modos para não quebrar as animações paradas. modes = { { name = "Perto", distanceScale = 0.78, heightAdd = -0.08, lookYAdd = -0.06, fovAdd = -1, }, { name = "Normal", distanceScale = 1.00, heightAdd = 0.00, lookYAdd = 0.00, fovAdd = 0, }, { name = "Longe", distanceScale = 1.20, heightAdd = 0.16, lookYAdd = 0.15, fovAdd = 2, }, { name = "Muito longe", distanceScale = 1.42, heightAdd = 0.28, lookYAdd = 0.26, fovAdd = 4, }, } }, mouseOrbit = { enabled = true, -- IMPORTANTE: -- Com setCameraMatrix ativo, o mouse nativo não controla a câmera sozinho. -- MODO SEGURO: por padrão não usa cursor invisível, porque showCursor(true) -- trava/atrapalha a dirigibilidade nativa do GTA/MTA. -- Captura real de mouse só é possível ativando captureCursor=true, mas é experimental. captureCursor = true, -- v1.0.41: captura real ligada; controles do veículo são emulados por script cursorAlpha = 0, recenterCursor = true, keepControlsEnabled = true, -- usa showCursor(true, false) quando disponível -- v1.0.76: MouseOrbit 8D com retorno automático. -- Permite olhar para traseira, frente, laterais e ângulos altos/baixos com o mouse. -- Ao parar de mover o mouse, volta suavemente para a traseira. maxYaw = 180.0, maxPitchUp = 44.0, maxPitchDown = -32.0, -- Sensibilidade real do mouse capturado. sensitivityX = 0.125, sensitivityY = 0.070, invertHorizontal = true, -- v1.0.42: corrige mouse esquerda/direita invertido -- Suavização/retorno. inputSmooth = 18.0, returnSmooth = 5.8, returnDelayMs = 850, autoReturn = true, -- v1.0.76: volta sozinho após não detectar movimento do mouse -- Look livre: quando mouseOrbit está ativo, o ponto de foco também acompanha a órbita. lookWithOrbit = true, pitchLookInfluence = 2.70, pitchCameraInfluence = 0.28, -- Filtros contra ruído e mini saltos. noiseDeadzone = 0.0015, maxDeltaPerFrame = 8.0, bigJumpIgnore = 120.0, -- Reduz influência automática de curva enquanto o mouse orbit está ativo. reduceTurnVision = 0.72, -- Quando Q/E ou Mouse3 estiverem pressionados, esses controles têm prioridade. disableWhenManualView = true, -- Só aplica enquanto estiver em gameplay, não na idle cinematográfica. disableOnIdle = true, notify = true, }, driveEmulation = { -- Nível 3: quando o cursor invisível está capturando o mouse, -- o veículo é comandado por script para não perder dirigibilidade. enabled = true, onlyWhenMouseCapture = true, clearWhenDisabled = true, blockWhenTyping = true, -- Teclas padrão. Isso mantém o sistema leve e evita depender do controle nativo -- caso o cursor esteja interceptando input. keys = { accelerate = { "w", "arrow_u" }, brake_reverse = { "s", "arrow_d" }, vehicle_left = { "a", "arrow_l" }, vehicle_right = { "d", "arrow_r" }, handbrake = { "space" }, horn = { "h" }, vehicle_fire = { "lctrl", "rctrl", "mouse1" }, vehicle_secondary_fire = { "lalt", "ralt", "mouse2" }, -- v1.0.83: controles especiais para veículos aéreos. -- Avião/helicóptero usam setas ↑/↓ para subir/descer, A/D + setas ←/→ para laterais -- e Q/E continuam como roll/giro nativo da aeronave, sem ativar câmera lateral. -- Q/E também ficam nesta lista para o bind down/up capturar corretamente mesmo com cursor invisível. special_control_left = { "a", "arrow_l", "q" }, special_control_right = { "d", "arrow_r", "e" }, special_control_up = { "arrow_u", "num_8" }, special_control_down = { "arrow_d", "num_2" }, -- v1.0.81: em aviões/helicópteros, frente/trás real também precisa -- reenviar steer_forward/steer_back. Em alguns veículos aéreos do MTA, -- somente special_control_up/down não move o nariz para frente/trás. steer_forward = { "arrow_u", "num_8" }, steer_back = { "arrow_d", "num_2" }, enter_exit = { "f", "enter", "num_enter" }, }, -- Também tenta ler controles nativos quando eles ainda respondem. useNativeControlStateToo = false, -- v1.0.41: NÃO ler getControlState aqui, pois ele pode ler o próprio setPedControlState e prender tecla -- v1.0.41: guarda DOWN/UP real das teclas e também confere getKeyState por frame. -- Isso evita controle preso quando o cursor invisível intercepta input. useHeldStateTable = true, useKeyStatePolling = true, updateOnlyOnChange = false, -- v1.0.41: força sincronização todo frame para não deixar tecla presa notify = true, }, turnVision = { enabled = true, minSpeed = 12.0, fullSpeed = 95.0, inputStrength = 0.30, yawRateStrength = 0.24, cameraSideShift = 0.18, lookSideShift = 0.46, frontLookBoost = 0.68, maxCameraSide = 0.28, maxLookSide = 0.62, maxFrontBoost = 0.85, smooth = 9.5, invert = false, }, cornerShake = { enabled = true, maxPosition = 0.0035, -- v1.0.89 maxLook = 0.0019, frequency = 4.2, curvePower = 2.35, smooth = 10.0, minSpeed = 35.0, fullSpeed = 125.0, }, -- v1.0.75: shake de asfalto/suspensão mais perceptível em irregularidades fortes. -- Continua leve no asfalto normal, mas reage melhor a trilho, guia, lombada e pancada de suspensão. roadShake = { enabled = true, -- Vibração constante leve enquanto pilota. maxPosition = 0.0072, -- v1.0.89: shake geral um pouco menor maxLook = 0.0035, frequency = 9.2, minSpeed = 8.0, fullSpeed = 115.0, speedInfluence = 0.34, -- Reação a impacto vertical/suspensão. verticalVelocityInfluence = 0.34, verticalAccelInfluence = 0.54, rotationImpactInfluence = 0.27, impactDecay = 8.2, impactFrequency = 13.5, impactPosition = 0.015, impactLook = 0.0072, impactThreshold = 0.034, }, -- v1.0.75: acompanha inclinação de subida/descida sem girar com o roll do veículo. -- Resolve o problema da câmera ficar com altura fixa em ladeira. slopeFollow = { enabled = true, pitchInfluence = 0.82, -- 0 = câmera plana, 1 = acompanha totalmente o pitch da pista maxForwardZ = 0.42, -- limite contra capotamento/lombada extrema lookPitchInfluence = 0.72, }, -- v1.0.75: rotação real da câmera baseada no roll do veículo. -- Agora não é só ajuste de altura lateral: o parâmetro ROLL do setCameraMatrix -- gira a tela junto com a inclinação lateral do carro. Se capotar, a câmera inverte. rotationFollow = { enabled = true, -- Pequeno ajuste vertical lateral para a câmera sentir banco/suspensão sem exagero. sideZInfluence = 0.14, maxSideZ = 0.18, lookSideZInfluence = 0.05, maxLookSideZ = 0.05, -- Roll real da câmera. Em uso normal fica sutil; em capotamento permite inverter. cameraRollEnabled = true, cameraRollInfluence = 0.55, maxCameraRoll = 13.0, capsizeRollInfluence = 1.0, maxCapsizeRoll = 180.0, upsideDownUpZ = -0.18, rollSmooth = 10.0, invertCameraRoll = false, }, -- v1.0.88: modo produção fluido. -- Mantém apenas otimizações seguras por cache interno, sem pular frames da câmera. -- Não usa target-cache agressivo, não pula setCameraMatrix e não atrasa atualização do alvo. performance = { naturalFluidMode = true, cacheVehicleMatrix = true, cacheVehicleRotation = true, cacheVehicleVelocity = true, -- evita múltiplos getElementVelocity no mesmo frame fovMinDelta = 0.05, -- leve economia sem afetar fluidez visual -- Estes campos existem para deixar claro que esta versão NÃO usa otimização agressiva. productionMode = false, allowMatrixFrameSkip = false, targetUpdateIntervalMs = 0, rollUpdateIntervalMs = 0, driveUpdateIntervalMs = 0, matrixApplyIntervalMs = 0, }, -- v1.0.78: perfis exclusivos por tipo de veículo. -- A câmera de carro continua igual. Aviões/helicópteros/barcos/trens recebem -- distância, FOV, força G, roll e shake próprios para não parecer câmera de automóvel. specialVehicles = { enabled = true, detectByType = true, detectByModel = true, -- Caso você queira forçar algum modelo para um perfil específico: -- byModel = { [592] = "plane", [487] = "helicopter" } byModel = {}, profiles = { plane = { disableIdle = true, camera = { forwardCamOffset = { x = 0.0, y = -17.5, z = 5.8 }, forwardLookOffset = { x = 0.0, y = 15.0, z = 1.55 }, reverseCamOffset = { x = 0.0, y = 14.0, z = 4.7 }, reverseLookOffset = { x = 0.0, y = -8.5, z = 1.35 }, positionSmooth = 5.6, lookSmooth = 7.0, reverseSmooth = 4.6, accelPullMax = 0.18, brakePushMax = 0.14, accelSmooth = 4.5, }, fov = { default = 78, max = 102, speedForMax = 360, smooth = 4.2 }, mouseOrbit = { maxYaw = 180.0, maxPitchUp = 74.0, maxPitchDown = -64.0, sensitivityX = 0.105, sensitivityY = 0.060, inputSmooth = 14.0, returnSmooth = 3.2, returnDelayMs = 1200, pitchLookInfluence = 4.0, pitchCameraInfluence = 0.38, }, gForce = { minSpeed = 25.0, deadzone = 8.0, accelPullScale = 0.0038, -- v1.0.89 brakePushScale = 0.0030, -- v1.0.89 maxBackwardPull = 0.28, -- v1.0.89 maxForwardPush = 0.20, -- v1.0.89 lateralCamScale = 0.0033, -- v1.0.89 lateralLookScale = 0.0051, -- v1.0.89 maxSideOffset = 0.24, -- v1.0.89 maxLookSide = 0.44, -- v1.0.89 pitchLookScale = 0.0022, -- v1.0.89 maxPitchLook = 0.17, -- v1.0.89 -- v1.0.89 smooth = 5.2, resetSmooth = 4.2, }, turnVision = { enabled = false }, cornerShake = { enabled = false }, roadShake = { enabled = false }, slopeFollow = { enabled = true, pitchInfluence = 1.0, maxForwardZ = 1.25, lookPitchInfluence = 1.0 }, rotationFollow = { enabled = true, sideZInfluence = 0.04, maxSideZ = 0.08, lookSideZInfluence = 0.02, maxLookSideZ = 0.03, cameraRollEnabled = true, cameraRollInfluence = 1.0, maxCameraRoll = 180.0, capsizeRollInfluence = 1.0, maxCapsizeRoll = 180.0, upsideDownUpZ = -0.10, rollSmooth = 5.5, }, }, helicopter = { disableIdle = true, camera = { forwardCamOffset = { x = 0.0, y = -11.5, z = 4.25 }, forwardLookOffset = { x = 0.0, y = 7.5, z = 1.20 }, reverseCamOffset = { x = 0.0, y = 9.2, z = 3.55 }, reverseLookOffset = { x = 0.0, y = -5.5, z = 1.05 }, positionSmooth = 5.2, lookSmooth = 6.4, reverseSmooth = 4.5, accelPullMax = 0.12, brakePushMax = 0.10, accelSmooth = 4.2, }, fov = { default = 76, max = 96, speedForMax = 220, smooth = 4.0 }, mouseOrbit = { maxYaw = 180.0, maxPitchUp = 70.0, maxPitchDown = -58.0, sensitivityX = 0.105, sensitivityY = 0.058, inputSmooth = 13.0, returnSmooth = 3.0, returnDelayMs = 1250, pitchLookInfluence = 3.6, pitchCameraInfluence = 0.34, }, gForce = { minSpeed = 8.0, deadzone = 5.5, accelPullScale = 0.0027, -- v1.0.89 brakePushScale = 0.0023, -- v1.0.89 maxBackwardPull = 0.18, -- v1.0.89 maxForwardPush = 0.15, -- v1.0.89 lateralCamScale = 0.0028, -- v1.0.89 lateralLookScale = 0.0044, -- v1.0.89 maxSideOffset = 0.18, -- v1.0.89 maxLookSide = 0.34, -- v1.0.89 pitchLookScale = 0.0017, -- v1.0.89 maxPitchLook = 0.17, -- v1.0.89 smooth = 4.8, resetSmooth = 3.8, }, turnVision = { enabled = false }, cornerShake = { enabled = false }, roadShake = { enabled = false }, slopeFollow = { enabled = true, pitchInfluence = 0.65, maxForwardZ = 0.65, lookPitchInfluence = 0.55 }, rotationFollow = { enabled = true, sideZInfluence = 0.06, maxSideZ = 0.10, lookSideZInfluence = 0.02, maxLookSideZ = 0.03, cameraRollEnabled = true, cameraRollInfluence = 0.72, maxCameraRoll = 85.0, capsizeRollInfluence = 1.0, maxCapsizeRoll = 180.0, upsideDownUpZ = -0.12, rollSmooth = 5.0, }, }, boat = { disableIdle = true, camera = { forwardCamOffset = { x = 0.0, y = -9.0, z = 3.2 }, forwardLookOffset = { x = 0.0, y = 5.2, z = 0.95 }, positionSmooth = 6.4, lookSmooth = 7.4, }, fov = { default = 74, max = 90, speedForMax = 160, smooth = 4.8 }, roadShake = { enabled = false }, cornerShake = { enabled = false }, slopeFollow = { enabled = true, pitchInfluence = 0.35, maxForwardZ = 0.35, lookPitchInfluence = 0.35 }, rotationFollow = { cameraRollEnabled = true, cameraRollInfluence = 0.42, maxCameraRoll = 18.0, rollSmooth = 5.0 }, }, train = { disableIdle = true, mouseOrbit = { maxYaw = 150.0, maxPitchUp = 46.0, maxPitchDown = -32.0, returnDelayMs = 1100 }, fov = { default = 74, max = 88, speedForMax = 180, smooth = 5.0 }, turnVision = { enabled = false }, cornerShake = { enabled = false }, roadShake = { enabled = true, maxPosition = 0.0045, maxLook = 0.0022, frequency = 9.0 }, rotationFollow = { cameraRollEnabled = false }, }, } }, manualViews = { enabled = true, sideSmooth = 10.5, rearSmooth = 9.5, sideCam = { x = 4.55, y = -4.65, z = 2.12 }, sideLook = { x = 0.85, y = 1.65, z = 0.88 }, rearReleaseMs = 180, rearCamOffset = { x = 0.0, y = 6.15, z = 2.12 }, rearLookOffset = { x = 0.0, y = -3.15, z = 0.84 }, useAlternativeControls = true, }, safety = { maxTiltForEffects = 47.0, resetFovOnStopResource = true, forceFullRestoreOnExit = true, -- v1.0.49: ao sair do veículo, restaura cursor/controles/câmera agressivamente kickPedCameraOnExit = false, -- v1.0.63: desativado; não usamos mais setCameraMatrix após sair do veículo detachCameraDuringExit = false, -- v1.0.64: não usamos câmera temporária na saída; bloqueamos saída em movimento exitDetachDurationMs = 0, -- v1.0.64: impede sair/pular do veículo enquanto estiver em movimento. -- Isso evita o bug visual da câmera presa no carro durante a animação de saída em velocidade. blockExitWhileMoving = true, blockExitMinSpeed = 5.0, -- km/h exitBlockNotifyCooldownMs = 1800, exitBlockMessage = "Pare o veículo para sair.", }, } local IdleConfig = { enabled = true, idleTimeToStart = 5000, minSpeedToCancel = 1.5, closeCooldown = 1800, -- menor porque agora não devolve para câmera nativa transitionTime = 2600, -- v1.0.25: trava global de altura da idle para eliminar o efeito de "nascer de cima". maxIdleHeight = 1.58, minIdleHeight = 0.46, heightPenaltyInSearch = 4.5, -- penaliza ponto alto ao escolher onde entrar no loop cameraDistanceScale = 1.08, orbitDistanceScale = 1.08, detailDistanceScale = 1.22, progressiveFlow = true, sameSideChance = 45, balancedVehicleCoverage = true, sideBalanceTolerance = 1, smoothTransitionMin = 2800, smoothTransitionMax = 4200, seamlessProfileChanges = true, -- nova animação nasce da última posição real da câmera profileStartSearchSamples = 16, -- procura dentro da próxima animação o ponto mais próximo da câmera atual reverseProfileIfCloserToEnd = true, -- se o ponto mais próximo for o final, inverte o caminho para continuar fluido minRemainingProfileProgress = 0.42, -- usado apenas em perfis não-loop megaLoopLocked = true, -- mantém o mesmo mega loop rodando continuamente durante a sessão idle megaLoopStartSearchSamples = 48, -- acha o ponto mais próximo do loop ao entrar na idle cancelWhenCursorVisible = true, forceVehicleStreamable = true, enableTestCommands = true, -- v1.0.25: 2 loops gigantes, porém somente baixos/laterais. -- Não existe mais ponto alto/drone/crane no começo, fim ou meio do loop. -- O loop inteiro fica limitado por maxIdleHeight para não começar de cima e descer. profiles = { { name = "Low Loop 01 - Lateral Showcase Around Car", mode = "mega_loop", weight = 1, durationMin = 145000, durationMax = 145000, loopStyle = "low_showcase", shake = 0.00065, -- v1.0.89 lookOffset = { x = 0.0, y = 0.0, z = 0.72 } }, { name = "Low Loop 02 - Detail Flow Around Car", mode = "mega_loop", weight = 1, durationMin = 155000, durationMax = 155000, loopStyle = "low_detail_flow", shake = 0.00075, -- v1.0.89 lookOffset = { x = 0.0, y = 0.0, z = 0.68 } } } } local state = { active = false, vehicle = nil, lastTick = getTickCount(), currentCam = nil, currentLook = nil, entryFrom = nil, entryStartTick = 0, currentFov = Config.fov.default, viewModeIndex = 1, reverseBlend = 0, manualSideBlend = 0, manualSide = 0, manualRearBlend = 0, rearPeekUntil = 0, mouse3Held = false, accelEffect = 0, turnAmount = 0, shakeAmount = 0, cameraRoll = 0, roadShakeImpact = 0, lastRoadVz = nil, lastRoadRx = nil, lastRoadRy = nil, -- v1.0.78: tipo especial atual aplicado em runtime: default/plane/helicopter/boat/train. currentVehicleSpecialKey = "default", lastSpeed = 0, lastYaw = nil, yawRate = 0, -- v1.0.77: força G refinada/cache de aceleração local. lastForwardSpeed = nil, lastSideSpeed = nil, gForceY = 0, gForceSide = 0, gForceLookSide = 0, gForceLookZ = 0, -- MouseOrbit customizado manualYaw = 0, manualYawTarget = 0, lastMouseOrbitTick = 0, lastPedCamRot = nil, lastMouseVehicleYaw = nil, lastManualYawApplied = 0, manualPitch = 0, manualPitchTarget = 0, mouseOrbitActive = false, mouseCaptureActive = false, mouseCaptureOwned = false, ignoreNextCursorMove = false, -- v1.0.41: estado físico das teclas de movimento capturado por bindKey down/up. driveKeysHeld = {}, emulatedControls = {}, -- v1.0.49: janela nativa pós-desembarque. -- Enquanto ativa, o render NÃO aplica setCameraMatrix nem reinicia a câmera. nativeRestoreUntil = 0, pendingVehicleExit = false, exitLockUntil = 0, enterExitPulseUntil = 0, -- v1.0.63: câmera temporária durante a animação de saída. -- Resolve o caso do MTA ainda considerar o player dentro do carro por ~1-2s. exitDetachUntil = 0, exitDetachVehicle = nil, } local idle = { active = false, idleStartTick = false, lastCloseTick = 0, currentProfile = nil, profileStartTick = 0, profileDuration = 6000, transitionStartTick = 0, transitionDuration = IdleConfig.transitionTime, fromCam = nil, fromLook = nil, lastCam = nil, lastLook = nil, preferredSide = nil, sideStats = { left = 0, right = 0 }, lastProfileName = nil, } local function clamp(v, a, b) if v < a then return a end if v > b then return b end return 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) end local function randomFloat(a, b) return a + math.random() * (b - a) end local function atan2Safe(y, x) y, x = y or 0, x or 0 if x > 0 then return math.atan(y / x) end if x < 0 and y >= 0 then return math.atan(y / x) + math.pi end if x < 0 and y < 0 then return math.atan(y / x) - math.pi end if x == 0 and y > 0 then return math.pi / 2 end if x == 0 and y < 0 then return -math.pi / 2 end return 0 end -- v1.0.75: cache por frame para diminuir chamadas pesadas repetidas no client. -- getElementMatrix/getElementRotation eram chamados várias vezes no mesmo onClientPreRender -- por slope, roadShake, look/cam e tilt. Agora ficam cacheados por tick + veículo. local perfCache = { matrixVehicle = nil, matrixTick = -1, matrix = nil, rotVehicle = nil, rotTick = -1, rx = 0, ry = 0, rz = 0, velVehicle = nil, velTick = -1, vx = 0, vy = 0, vz = 0, } local function getCachedElementMatrix(vehicle) if not isElement(vehicle) then return nil end local cfg = Config.performance or {} local tick = getTickCount() if cfg.cacheVehicleMatrix == false then return getElementMatrix(vehicle) end if perfCache.matrixVehicle ~= vehicle or perfCache.matrixTick ~= tick then perfCache.matrixVehicle = vehicle perfCache.matrixTick = tick perfCache.matrix = getElementMatrix(vehicle) end return perfCache.matrix end local function getCachedElementRotation(vehicle) if not isElement(vehicle) then return 0, 0, 0 end local cfg = Config.performance or {} local tick = getTickCount() if cfg.cacheVehicleRotation == false then return getElementRotation(vehicle) end if perfCache.rotVehicle ~= vehicle or perfCache.rotTick ~= tick then perfCache.rotVehicle = vehicle perfCache.rotTick = tick local rx, ry, rz = getElementRotation(vehicle) perfCache.rx, perfCache.ry, perfCache.rz = rx or 0, ry or 0, rz or 0 end return perfCache.rx, perfCache.ry, perfCache.rz end local function getCachedElementVelocity(vehicle) if not isElement(vehicle) then return 0, 0, 0 end local cfg = Config.performance or {} local tick = getTickCount() if cfg.cacheVehicleVelocity == false then return getElementVelocity(vehicle) end if perfCache.velVehicle ~= vehicle or perfCache.velTick ~= tick then perfCache.velVehicle = vehicle perfCache.velTick = tick local vx, vy, vz = getElementVelocity(vehicle) perfCache.vx, perfCache.vy, perfCache.vz = vx or 0, vy or 0, vz or 0 end return perfCache.vx, perfCache.vy, perfCache.vz end local function angleDiff(a, b) return (a - b + 180) % 360 - 180 end local function deepCopy(obj) if type(obj) ~= "table" then return obj end local result = {} for k, v in pairs(obj) do result[k] = deepCopy(v) end return result end -- state já foi declarada acima; não redeclarar aqui. -- ========================================================= -- PERFIS RUNTIME POR TIPO DE VEÍCULO -- ========================================================= local RuntimeBaseConfig = nil local RuntimeConfigKeys = { "camera", "fov", "gForce", "turnVision", "cornerShake", "roadShake", "slopeFollow", "rotationFollow", "mouseOrbit", "manualViews", "performance" } local function mergeRuntimeTable(dst, src) if type(dst) ~= "table" or type(src) ~= "table" then return end for k, v in pairs(src) do if type(v) == "table" and type(dst[k]) == "table" then mergeRuntimeTable(dst[k], v) else dst[k] = deepCopy(v) end end end local function captureRuntimeBaseConfig() if RuntimeBaseConfig then return end RuntimeBaseConfig = {} for _, key in ipairs(RuntimeConfigKeys) do RuntimeBaseConfig[key] = deepCopy(Config[key]) end end local function restoreRuntimeBaseConfig() captureRuntimeBaseConfig() for _, key in ipairs(RuntimeConfigKeys) do Config[key] = deepCopy(RuntimeBaseConfig[key]) end end local function getVehicleSpecialKey(vehicle) local special = Config.specialVehicles if not special or not special.enabled or not isElement(vehicle) then return "default" end if special.detectByModel and type(special.byModel) == "table" then local model = getElementModel(vehicle) local mapped = special.byModel[model] or special.byModel[tostring(model)] if mapped then return tostring(mapped) end end if special.detectByType and type(getVehicleType) == "function" then local ok, vehicleType = pcall(getVehicleType, vehicle) if ok and vehicleType then vehicleType = tostring(vehicleType):lower() if vehicleType == "plane" then return "plane" end if vehicleType == "helicopter" then return "helicopter" end if vehicleType == "boat" then return "boat" end if vehicleType == "train" then return "train" end end end return "default" end local function getCurrentSpecialProfile() local special = Config.specialVehicles if not special or not special.profiles then return nil end local key = state and state.currentVehicleSpecialKey or nil if not key or key == "default" then return nil end return special.profiles[key] end local function applyVehicleSpecialRuntimeConfig(vehicle) captureRuntimeBaseConfig() local key = getVehicleSpecialKey(vehicle) if state and state.currentVehicleSpecialKey == key then return end restoreRuntimeBaseConfig() local special = Config.specialVehicles local profile = special and special.profiles and special.profiles[key] if profile then for _, cfgKey in ipairs(RuntimeConfigKeys) do if type(profile[cfgKey]) == "table" and type(Config[cfgKey]) == "table" then mergeRuntimeTable(Config[cfgKey], profile[cfgKey]) end end end if state then state.currentVehicleSpecialKey = key end end local function safeGetEasingValue(progress, easingType) progress = clamp(progress or 0, 0, 1) local value = getEasingValue(progress, easingType or "InOutQuad") if type(value) ~= "number" then value = getEasingValue(progress, "InOutQuad") end if type(value) ~= "number" then value = progress end return value end local function getSpeedKMH(vehicle) if not isElement(vehicle) then return 0 end local vx, vy, vz = getCachedElementVelocity(vehicle) return math.sqrt(vx * vx + vy * vy + vz * vz) * 180 end local function notifyExitBlocked() local now = getTickCount() local cooldown = (Config.safety and Config.safety.exitBlockNotifyCooldownMs) or 1800 if now - (state.lastExitBlockNotifyTick or 0) < cooldown then return end state.lastExitBlockNotifyTick = now local msg = (Config.safety and Config.safety.exitBlockMessage) or "Pare o veículo para sair." local notified = false -- Export client-side informado pelo usuário: -- exports['[HS]Notificacoes']:notify('mensagem', 'warning') if exports and exports['[HS]Notificacoes'] and type(exports['[HS]Notificacoes'].notify) == "function" then local ok = pcall(function() exports['[HS]Notificacoes']:notify(msg, 'warning') end) notified = ok == true end -- Fallback opcional para versões antigas, caso o resource [HS]Notificacoes não esteja iniciado. if not notified and exports and exports['[HS]Notificacao'] and type(exports['[HS]Notificacao'].notify) == "function" then local ok = pcall(function() exports['[HS]Notificacao']:notify(localPlayer, msg, 'warning') end) notified = ok == true end if not notified then outputChatBox("#ff0048[Veículo] #ffffff" .. msg, 255, 255, 255, true) end end local getLocalOccupiedVehicleAndSeat local function shouldBlockVehicleExit(vehicle) if not (Config.safety and Config.safety.blockExitWhileMoving) then return false end if not isElement(vehicle) then return false end -- v1.0.65: bloqueia saída em movimento para qualquer ocupante local, -- motorista ou passageiro. Isso não ativa câmera custom no passageiro; -- só impede o exit quando o veículo ainda está andando. local occupiedVehicle = getPedOccupiedVehicle(localPlayer) if occupiedVehicle ~= vehicle then return false end local _, seat = getLocalOccupiedVehicleAndSeat() if seat == nil then return false end local speed = getSpeedKMH(vehicle) return speed > ((Config.safety and Config.safety.blockExitMinSpeed) or 5.0) end local function blockVehicleExitNow(reason, vehicle) vehicle = vehicle or getPedOccupiedVehicle(localPlayer) if not shouldBlockVehicleExit(vehicle) then return false end -- Não deixa o nosso sistema de emulação mandar enter_exit enquanto estiver em movimento. if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, "enter_exit", false) end if state.emulatedControls then state.emulatedControls.enter_exit = false end -- Limpa apenas as teclas de saída para evitar loop, sem mexer em W/A/S/D/freio. if state.driveKeysHeld then state.driveKeysHeld["f"] = false state.driveKeysHeld["enter"] = false state.driveKeysHeld["num_enter"] = false end notifyExitBlocked() if Config.debug then outputDebugString("[DynamicCamera] saída bloqueada em movimento: " .. tostring(reason)) end return true end local function getVehicleYaw(vehicle) local _, _, rz = getCachedElementRotation(vehicle) return rz or 0 end local function getForwardVectorFromYaw(yawDeg) local r = math.rad(yawDeg) return -math.sin(r), math.cos(r), 0 end local function getSignedForwardSpeedKMH(vehicle, yawDeg) if not isElement(vehicle) then return 0 end local vx, vy = getCachedElementVelocity(vehicle) local fx, fy = getForwardVectorFromYaw(yawDeg) return (vx * fx + vy * fy) * 180 end local function localToWorldYaw(vehicle, ox, oy, oz, yawDeg, isLookPoint) if not isElement(vehicle) then return nil end local px, py, pz = getElementPosition(vehicle) local r = math.rad(yawDeg) local rightX, rightY = math.cos(r), math.sin(r) local forwardX, forwardY = -math.sin(r), math.cos(r) -- v1.0.75: correção de subidas/descidas. -- Antes a câmera usava somente yaw e Z fixo, então em ladeiras ela parecia ficar alta/fora do ângulo. -- Agora só o eixo frente/trás acompanha o pitch da matriz do veículo. -- O roll continua controlado separadamente para evitar enjoo/câmera girando em capotamento. local matrix = nil local slopeZ = 0 local sf = Config.slopeFollow if sf and sf.enabled then matrix = matrix or getCachedElementMatrix(vehicle) if matrix and matrix[2] then local forwardZ = clamp(matrix[2][3] or 0, -(sf.maxForwardZ or 0.42), (sf.maxForwardZ or 0.42)) local influence = isLookPoint and (sf.lookPitchInfluence or sf.pitchInfluence or 0.75) or (sf.pitchInfluence or 0.82) slopeZ = forwardZ * (oy or 0) * influence end end -- v1.0.75: ajuste vertical lateral complementar. -- O roll visual real é aplicado no setCameraMatrix; aqui fica só um micro ajuste -- de altura lateral para sentir suspensão/banco sem aumentar custo. local rollZ = 0 local rf = Config.rotationFollow if rf and rf.enabled then matrix = matrix or getCachedElementMatrix(vehicle) if matrix and matrix[1] then local rightZ = clamp(matrix[1][3] or 0, -1, 1) local influence = isLookPoint and (rf.lookSideZInfluence or 0.05) or (rf.sideZInfluence or 0.14) local limit = isLookPoint and (rf.maxLookSideZ or 0.05) or (rf.maxSideZ or 0.18) rollZ = clamp(rightZ * (ox or 0) * influence, -limit, limit) end end return px + rightX * ox + forwardX * oy, py + rightY * ox + forwardY * oy, pz + oz + slopeZ + rollZ 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 -- v1.0.57: leitura segura de assento. -- Retorna vehicle, seat. No MTA, seat 0 = motorista. getLocalOccupiedVehicleAndSeat = function() local vehicle = getPedOccupiedVehicle(localPlayer) if not vehicle or not isElement(vehicle) then return nil, nil end if isPedDead(localPlayer) then return nil, nil end for seat = 0, 7 do if getVehicleOccupant(vehicle, seat) == localPlayer then return vehicle, seat end end return vehicle, nil end local function isLocalPlayerDriver() local vehicle, seat = getLocalOccupiedVehicleAndSeat() return vehicle ~= nil and seat == 0 end local function isLocalPlayerPassenger() local vehicle, seat = getLocalOccupiedVehicleAndSeat() return vehicle ~= nil and seat ~= nil and seat ~= 0 end local function getControlStateSafe(controlName) if type(getControlState) ~= "function" then return false end local ok, result = pcall(getControlState, controlName) return ok and result == true end local function getKeyStateSafe(keyName) if type(getKeyState) ~= "function" then return false end local ok, result = pcall(getKeyState, keyName) return ok and result == true end -- v1.0.63: pré-declara setFovSafe porque funções de restauração acima/abaixo -- podem chamar ela antes da definição real no arquivo. local setFovSafe local function callBoolFunctionSafe(name) local fn = _G[name] if type(fn) ~= "function" then return false end local ok, result = pcall(fn) return ok and result == true end local function isTypingOrMenuOpen() if callBoolFunctionSafe("isChatBoxInputActive") then return true end if callBoolFunctionSafe("isConsoleActive") then return true end if callBoolFunctionSafe("isMainMenuActive") then return true end if callBoolFunctionSafe("isMTAWindowActive") then return true end return false end local function readDriveInput(controlName, keys) local cfg = Config.driveEmulation or {} local pressed = false -- v1.0.41: -- NÃO usamos getControlState como fonte principal quando a emulação está ativa. -- Motivo: setPedControlState pode fazer getControlState voltar true no próximo frame, -- criando loop onde a tecla fica presa mesmo após soltar W/A/S/D. -- 1) Tabela DOWN/UP capturada por bindKey/onClientKey. if cfg.useHeldStateTable ~= false and type(keys) == "table" then for _, key in ipairs(keys) do if state.driveKeysHeld and state.driveKeysHeld[key] == true then pressed = true break end end end -- 2) Polling físico por frame como confirmação. if not pressed and cfg.useKeyStatePolling ~= false and type(keys) == "table" then for _, key in ipairs(keys) do if getKeyStateSafe(key) then pressed = true break end end end -- 3) Fallback nativo só se você ativar manualmente no Config. -- Por padrão fica false para não prender controle em loop. if not pressed and cfg.useNativeControlStateToo == true then pressed = getControlStateSafe(controlName) end return pressed == true end -- v1.0.70: leitura direta dos controles de direção/movimento para não perder A/D -- enquanto Q/E ou Mouse3 estão sendo usados para visão lateral/ré. local function readDriveInputDirect(controlName) local keys = Config.driveEmulation and Config.driveEmulation.keys or {} return readDriveInput(controlName, keys[controlName]) end -- v1.0.81: em veículos aéreos, as setas ↑/↓ precisam acionar tanto -- special_control_up/down quanto steer_forward/steer_back. Alguns modelos usam -- um, outros usam o outro para frente/trás/pitch. A/D e setas ←/→ vão para -- vehicle_left/right e special_control_left/right. local function isAirSpecialVehicle() local key = state and state.currentVehicleSpecialKey or "default" return key == "plane" or key == "helicopter" end local DRIVE_CONTROL_LIST = { "accelerate", "brake_reverse", "vehicle_left", "vehicle_right", "handbrake", "horn", "vehicle_fire", "vehicle_secondary_fire", "special_control_left", "special_control_right", "special_control_up", "special_control_down", "steer_forward", "steer_back", "vehicle_look_left", "vehicle_look_right", "enter_exit" } local function setLocalControlState(controlName, value, force) if type(setPedControlState) ~= "function" then return end value = value == true state.emulatedControls = state.emulatedControls or {} -- v1.0.41: -- Durante gameplay normal reenviamos todos os estados todo frame. -- Durante limpeza usamos force=true para garantir false mesmo que o estado salvo diga false. if not force and Config.driveEmulation and Config.driveEmulation.updateOnlyOnChange ~= false then if state.emulatedControls[controlName] == value then return end end pcall(setPedControlState, localPlayer, controlName, value) state.emulatedControls[controlName] = value end local function clearDriveEmulation() -- v1.0.41: libera TODOS os controles conhecidos, não só os que estavam na tabela. -- Isso evita tecla presa após chat, console, resource stop, saída do veículo ou perda de foco. for _, controlName in ipairs(DRIVE_CONTROL_LIST) do setLocalControlState(controlName, false, true) end state.emulatedControls = {} state.driveEmulationActive = false end local function setDriveKeyHeld(keyName, isDown) if not keyName then return end state.driveKeysHeld = state.driveKeysHeld or {} state.driveKeysHeld[keyName] = isDown == true end local function registerDriveKeyStateBinds() local cfg = Config.driveEmulation if not cfg or not cfg.keys then return end local registered = {} for _, keyList in pairs(cfg.keys) do if type(keyList) == "table" then for _, keyName in ipairs(keyList) do if keyName and not registered[keyName] then registered[keyName] = true bindKey(keyName, "down", function() setDriveKeyHeld(keyName, true) end) bindKey(keyName, "up", function() setDriveKeyHeld(keyName, false) end) end end end end end local function clearDriveKeyHeldStates() state.driveKeysHeld = {} end -- v1.0.49: precisa ser declarado antes da função de restore para o Lua capturar o local correto. local releaseMouseCapture local function getSafeNativeCameraRotation(vehicle) -- Ao sair do veículo, qualquer manualYaw/manualPitch do MouseOrbit precisa morrer na hora. -- Usar a rotação do veículo evita a câmera nativa nascer olhando para o ângulo manual antigo. if isElement(vehicle) then local yaw = getVehicleYaw(vehicle) if yaw then return yaw end end local _, _, pedRot = getElementRotation(localPlayer) return pedRot or getPedCameraRotation(localPlayer) or 0 end local function snapNativeCameraInstant(reason, vehicle) -- v1.0.61: -- NÃO força setPedCameraRotation aqui. -- Forçar rotação por timers era o que deixava a câmera com resíduo/travada -- olhando para o carro por 1~2 segundos após sair. -- Agora apenas mata a câmera custom e devolve o controle para a câmera nativa do MTA. state.exitSnapRotation = nil -- Zera todos os resíduos do mouse/câmera custom ANTES de devolver para a nativa. state.manualYaw = 0 state.manualYawTarget = 0 state.manualPitch = 0 state.manualPitchTarget = 0 state.lastMouseOrbitTick = 0 state.lastPedCamRot = nil state.lastMouseVehicleYaw = nil state.lastManualYawApplied = 0 state.mouseOrbitActive = false state.ignoreNextCursorMove = false state.currentCam = nil state.currentLook = nil state.entryFrom = nil state.reverseBlend = 0 state.manualSideBlend = 0 state.manualSide = 0 state.manualRearBlend = 0 state.rearPeekUntil = 0 state.mouse3Held = false state.accelEffect = 0 state.gForceY = 0 state.gForceSide = 0 state.gForceLookSide = 0 state.gForceLookZ = 0 state.lastForwardSpeed = nil state.lastSideSpeed = nil state.turnAmount = 0 state.shakeAmount = 0 state.cameraRoll = 0 state.roadShakeImpact = 0 state.lastRoadVz = nil state.lastRoadRx = nil state.lastRoadRy = nil idle.active = false idle.idleStartTick = false idle.currentProfile = nil idle.fromCam = nil idle.fromLook = nil idle.lastCam = nil idle.lastLook = nil setFovSafe(Config.fov.default) setCameraTarget(localPlayer) end local function kickCameraToPedAfterExit(reason) -- v1.0.63: não usamos mais nenhum setCameraMatrix depois que o player saiu. -- A câmera temporária só existe durante a animação de saída, antes do onClientVehicleExit. return end local function startExitDetachCamera(reason, vehicle, durationMs) if not (Config.safety and Config.safety.detachCameraDuringExit) then return end state.exitDetachUntil = math.max(state.exitDetachUntil or 0, getTickCount() + (durationMs or (Config.safety.exitDetachDurationMs or 650))) state.exitDetachVehicle = vehicle or state.vehicle -- Mata resíduos da câmera dinâmica imediatamente. state.manualYaw = 0 state.manualYawTarget = 0 state.manualPitch = 0 state.manualPitchTarget = 0 state.lastMouseOrbitTick = 0 state.mouseOrbitActive = false state.currentCam = nil state.currentLook = nil state.entryFrom = nil state.reverseBlend = 0 state.manualSideBlend = 0 state.manualSide = 0 state.manualRearBlend = 0 state.rearPeekUntil = 0 state.mouse3Held = false state.accelEffect = 0 state.gForceY = 0 state.gForceSide = 0 state.gForceLookSide = 0 state.gForceLookZ = 0 state.lastForwardSpeed = nil state.lastSideSpeed = nil state.turnAmount = 0 state.shakeAmount = 0 state.cameraRoll = 0 state.roadShakeImpact = 0 state.lastRoadVz = nil state.lastRoadRx = nil state.lastRoadRy = nil idle.active = false idle.idleStartTick = false idle.currentProfile = nil if releaseMouseCapture then releaseMouseCapture() end clearDriveEmulation() clearDriveKeyHeldStates() setFovSafe(Config.fov.default) end local function renderExitDetachCamera() local now = getTickCount() if not state.exitDetachUntil or state.exitDetachUntil <= now then return false end -- Se o MTA já confirmou que saiu, devolve para nativa agora e encerra. if not getPedOccupiedVehicle(localPlayer) then state.exitDetachUntil = 0 state.exitDetachVehicle = nil setFovSafe(Config.fov.default) setCameraTarget(localPlayer) return true end if releaseMouseCapture then releaseMouseCapture() end clearDriveEmulation() clearDriveKeyHeldStates() setFovSafe(Config.fov.default) -- Enquanto o MTA ainda mantém o ped tecnicamente no veículo, a câmera nativa -- tende a ficar presa no carro. Por isso usamos uma câmera curta baseada no ped, -- não na matriz do veículo, apenas durante a animação de saída. local px, py, pz = getElementPosition(localPlayer) local _, _, rz = getElementRotation(localPlayer) rz = rz or 0 local r = math.rad(rz) local backX, backY = math.sin(r), -math.cos(r) local camX, camY, camZ = px + backX * 3.2, py + backY * 3.2, pz + 1.45 local lookX, lookY, lookZ = px, py, pz + 0.95 setCameraMatrix(camX, camY, camZ, lookX, lookY, lookZ, 0, Config.fov.default) return true end local function restorePlayerDefaultsNow(reason) -- v1.0.49: restauração total ao sair do veículo/desativar. -- Objetivo: nunca deixar cursor invisível, tecla emulada ou câmera custom travando o player. if type(releaseMouseCapture) == "function" then releaseMouseCapture() else if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, 255) end if type(showCursor) == "function" then pcall(showCursor, false) end end -- Libera qualquer controle que o script possa ter pressionado. clearDriveEmulation() clearDriveKeyHeldStates() -- Força false mais de uma vez para evitar estado preso no mesmo frame do enter_exit. for _, controlName in ipairs(DRIVE_CONTROL_LIST) do if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, controlName, false) end end -- Reabilita controles nativos caso algum cursor/painel tenha afetado. if Config.safety and Config.safety.forceFullRestoreOnExit then if type(toggleAllControls) == "function" then pcall(toggleAllControls, true, true, true) end if type(guiSetInputEnabled) == "function" then pcall(guiSetInputEnabled, false) end if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, 255) end if type(showCursor) == "function" then pcall(showCursor, false) end end state.mouse3Held = false state.mouseOrbitActive = false state.manualYaw = 0 state.manualYawTarget = 0 state.manualPitch = 0 state.manualPitchTarget = 0 state.lastMouseOrbitTick = 0 state.rearPeekUntil = 0 state.manualRearBlend = 0 state.manualSideBlend = 0 state.manualSide = 0 state.reverseBlend = 0 idle.active = false idle.idleStartTick = false idle.currentProfile = nil snapNativeCameraInstant(reason, nil) if Config.debug then outputDebugString("[DynamicCamera] native restore: " .. tostring(reason)) end end local function scheduleRestorePlayerDefaults(reason) restorePlayerDefaultsNow(reason) -- Repete nos próximos frames porque o GTA/MTA pode processar o enter_exit depois do nosso handler. setTimer(function() restorePlayerDefaultsNow(tostring(reason) .. ":timer35") end, 35, 1) setTimer(function() restorePlayerDefaultsNow(tostring(reason) .. ":timer120") end, 120, 1) setTimer(function() restorePlayerDefaultsNow(tostring(reason) .. ":timer180") end, 180, 1) end local function forceNativeCameraWindow(reason, durationMs) durationMs = durationMs or 90 local now = getTickCount() -- v1.0.49: não limpamos enter_exit imediatamente, senão o GTA/MTA pode não receber -- o comando de desembarque. Abrimos uma janela onde a câmera custom para de renderizar, -- liberamos cursor/controles e pulsamos enter_exit por alguns frames. state.nativeRestoreUntil = math.max(state.nativeRestoreUntil or 0, now + durationMs) state.exitLockUntil = math.max(state.exitLockUntil or 0, now + durationMs) state.enterExitPulseUntil = math.max(state.enterExitPulseUntil or 0, now + 120) state.pendingVehicleExit = true if releaseMouseCapture then releaseMouseCapture() end if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, 255) end if type(showCursor) == "function" then pcall(showCursor, false) end if type(guiSetInputEnabled) == "function" then pcall(guiSetInputEnabled, false) end if type(toggleAllControls) == "function" then pcall(toggleAllControls, true, true, true) end -- Limpa todos os controles emulados, exceto enter_exit, que precisa ser enviado ao jogo. clearDriveEmulation() clearDriveKeyHeldStates() if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, "enter_exit", true) end local exitVehicle = state.vehicle snapNativeCameraInstant(reason, exitVehicle) startExitDetachCamera(reason, exitVehicle, Config.safety.exitDetachDurationMs or 650) state.active = false state.vehicle = nil setTimer(function() if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, "enter_exit", false) end end, 140, 1) end local function restoreImmediatelyForVehicleExitStart(reason, vehicle) -- v1.0.59: -- Usado quando o MTA começa a animação/ação de saída do veículo. -- Isso cobre F/Enter, pulo com veículo em movimento e saídas iniciadas por script. -- A câmera custom para ANTES do player realmente sair do banco. local now = getTickCount() state.nativeRestoreUntil = math.max(state.nativeRestoreUntil or 0, now + 90) state.exitLockUntil = math.max(state.exitLockUntil or 0, now + 90) state.pendingVehicleExit = true state.enterExitPulseUntil = 0 if releaseMouseCapture then releaseMouseCapture() end if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, 255) end if type(showCursor) == "function" then pcall(showCursor, false) end if type(guiSetInputEnabled) == "function" then pcall(guiSetInputEnabled, false) end if type(toggleAllControls) == "function" then pcall(toggleAllControls, true, true, true) end clearDriveEmulation() clearDriveKeyHeldStates() snapNativeCameraInstant(reason, vehicle or state.vehicle) startExitDetachCamera(reason, vehicle or state.vehicle, Config.safety.exitDetachDurationMs or 650) state.active = false state.vehicle = nil state.currentCam = nil state.currentLook = nil state.entryFrom = nil idle.active = false idle.idleStartTick = false idle.currentProfile = nil end local function updateDriveEmulation(vehicle) local cfg = Config.driveEmulation if not cfg or not cfg.enabled then if cfg == nil or Config.driveEmulation.clearWhenDisabled ~= false then clearDriveEmulation() end return end if not isElement(vehicle) or not state.active or idle.active then clearDriveEmulation() return end if cfg.onlyWhenMouseCapture and not state.mouseCaptureOwned then clearDriveEmulation() return end if cfg.blockWhenTyping and isTypingOrMenuOpen() then clearDriveEmulation() clearDriveKeyHeldStates() if releaseMouseCapture then releaseMouseCapture() end return end local keys = cfg.keys or {} local isAir = isAirSpecialVehicle() -- v1.0.81: ajuste específico para avião/helicóptero. -- W/S continuam como throttle/aceleração. -- Setas ↑/↓ são reenviadas como special_control_up/down E steer_forward/back, -- porque alguns modelos aéreos do MTA usam steer_* para frente/trás/pitch. -- A/D e setas ←/→ continuam controlando os lados em vehicle_* e special_control_*. local accelerateKeys = isAir and { "w" } or keys.accelerate local brakeKeys = isAir and { "s" } or keys.brake_reverse local leftKeys = isAir and { "a", "arrow_l" } or keys.vehicle_left local rightKeys = isAir and { "d", "arrow_r" } or keys.vehicle_right -- v1.0.83: em aeronaves, Q/E continuam funcionando como controle nativo de roll/giro. -- Enviamos Q/E para special_control_* e também para vehicle_look_* porque alguns modelos/controles -- do GTA/MTA usam um nome ou outro para o roll da aeronave. Esses controles NÃO acionam câmera lateral. local specialLeftKeys = isAir and { "q" } or keys.special_control_left local specialRightKeys = isAir and { "e" } or keys.special_control_right local airLookLeftKeys = isAir and { "q" } or nil local airLookRightKeys = isAir and { "e" } or nil local airForwardKeys = isAir and { "arrow_u", "num_8" } or keys.steer_forward local airBackKeys = isAir and { "arrow_d", "num_2" } or keys.steer_back local specialUpKeys = isAir and { "arrow_u", "num_8" } or keys.special_control_up local specialDownKeys = isAir and { "arrow_d", "num_2" } or keys.special_control_down local values = { accelerate = readDriveInput("accelerate", accelerateKeys), brake_reverse = readDriveInput("brake_reverse", brakeKeys), vehicle_left = readDriveInput("vehicle_left", leftKeys), vehicle_right = readDriveInput("vehicle_right", rightKeys), handbrake = readDriveInput("handbrake", keys.handbrake), horn = readDriveInput("horn", keys.horn), vehicle_fire = readDriveInput("vehicle_fire", keys.vehicle_fire), vehicle_secondary_fire = readDriveInput("vehicle_secondary_fire", keys.vehicle_secondary_fire), special_control_left = readDriveInput("special_control_left", specialLeftKeys), special_control_right = readDriveInput("special_control_right", specialRightKeys), special_control_up = readDriveInput("special_control_up", specialUpKeys), special_control_down = readDriveInput("special_control_down", specialDownKeys), steer_forward = readDriveInput("steer_forward", airForwardKeys), steer_back = readDriveInput("steer_back", airBackKeys), vehicle_look_left = isAir and readDriveInput("vehicle_look_left", airLookLeftKeys) or false, vehicle_look_right = isAir and readDriveInput("vehicle_look_right", airLookRightKeys) or false, enter_exit = readDriveInput("enter_exit", keys.enter_exit), } -- v1.0.70/v1.0.81: reforço de dirigibilidade durante Q/E/Mouse3. -- Mesmo usando visão lateral/ré, movimento e controles especiais continuam sendo reenviados. values.accelerate = readDriveInput("accelerate", accelerateKeys) values.brake_reverse = readDriveInput("brake_reverse", brakeKeys) values.vehicle_left = readDriveInput("vehicle_left", leftKeys) values.vehicle_right = readDriveInput("vehicle_right", rightKeys) values.handbrake = readDriveInput("handbrake", keys.handbrake) values.special_control_left = readDriveInput("special_control_left", specialLeftKeys) values.special_control_right = readDriveInput("special_control_right", specialRightKeys) values.special_control_up = readDriveInput("special_control_up", specialUpKeys) values.special_control_down = readDriveInput("special_control_down", specialDownKeys) values.steer_forward = readDriveInput("steer_forward", airForwardKeys) values.steer_back = readDriveInput("steer_back", airBackKeys) values.vehicle_look_left = isAir and readDriveInput("vehicle_look_left", airLookLeftKeys) or false values.vehicle_look_right = isAir and readDriveInput("vehicle_look_right", airLookRightKeys) or false -- v1.0.64: se estiver em movimento, bloqueia a saída e notifica. -- Isso evita o bug da câmera ficar presa no carro durante a animação de saída em velocidade. if values.enter_exit then if blockVehicleExitNow("enter_exit_emulation", vehicle) then return end -- Parado/quase parado: permite saída normal e restaura a câmera nativa. forceNativeCameraWindow("enter_exit_emulation", 260) return end -- Evita estados impossíveis causados por teclado fantasma. if values.vehicle_left and values.vehicle_right then values.vehicle_left = false values.vehicle_right = false end if values.special_control_left and values.special_control_right then values.special_control_left = false values.special_control_right = false end if values.special_control_up and values.special_control_down then values.special_control_up = false values.special_control_down = false end if values.vehicle_look_left and values.vehicle_look_right then values.vehicle_look_left = false values.vehicle_look_right = false end if values.steer_forward and values.steer_back then values.steer_forward = false values.steer_back = false end for controlName, value in pairs(values) do setLocalControlState(controlName, value) end state.driveEmulationActive = true end setFovSafe = function(value) value = clamp(value, 1, 179) local oldValue = state.currentFov state.currentFov = value -- v1.0.75: evita 3 chamadas de setCameraFieldOfView por frame quando a diferença é mínima. -- Reduz custo no client sem mudar perceptivelmente o FOV dinâmico. local minDelta = (Config.performance and Config.performance.fovMinDelta) or 0 if oldValue and math.abs(oldValue - value) < minDelta then return end if type(setCameraFieldOfView) == "function" then pcall(setCameraFieldOfView, "vehicle", value) pcall(setCameraFieldOfView, "vehicle_max", value) pcall(setCameraFieldOfView, "player", value) end end local function resetCameraSystem() setFovSafe(Config.fov.default) setCameraTarget(localPlayer) end local function stopDynamicCamera(reason) applyVehicleSpecialRuntimeConfig(nil) state.nativeRestoreUntil = math.max(state.nativeRestoreUntil or 0, getTickCount() + 120) state.pendingVehicleExit = false state.active = false state.vehicle = nil state.currentCam = nil state.currentLook = nil state.entryFrom = nil state.reverseBlend = 0 state.manualSideBlend = 0 state.manualSide = 0 state.manualRearBlend = 0 state.rearPeekUntil = 0 state.mouse3Held = false state.accelEffect = 0 state.gForceY = 0 state.gForceSide = 0 state.gForceLookSide = 0 state.gForceLookZ = 0 state.lastForwardSpeed = nil state.lastSideSpeed = nil state.turnAmount = 0 state.shakeAmount = 0 state.cameraRoll = 0 state.roadShakeImpact = 0 state.lastRoadVz = nil state.lastRoadRx = nil state.lastRoadRy = nil state.lastYaw = nil state.yawRate = 0 state.manualYaw = 0 state.manualYawTarget = 0 state.lastMouseOrbitTick = 0 state.lastPedCamRot = nil state.lastMouseVehicleYaw = nil state.lastManualYawApplied = 0 state.mouseOrbitActive = false idle.active = false idle.idleStartTick = false idle.currentProfile = nil idle.fromCam = nil idle.fromLook = nil -- v1.0.49: restaura absolutamente tudo ao padrão. restorePlayerDefaultsNow(reason) if Config.debug then outputDebugString("[DynamicCamera] stopped: " .. 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.lastTick = getTickCount() state.entryStartTick = getTickCount() local cx, cy, cz, lx, ly, lz = getCameraMatrix() if cx then state.entryFrom = { cam = { cx, cy, cz }, look = { lx, ly, lz } } else state.entryFrom = nil end state.currentCam = nil state.currentLook = nil state.lastSpeed = getSpeedKMH(vehicle) state.lastYaw = getVehicleYaw(vehicle) state.manualYaw = 0 state.manualYawTarget = 0 state.lastMouseOrbitTick = 0 state.lastPedCamRot = type(getPedCameraRotation) == "function" and getPedCameraRotation(localPlayer) or nil state.lastMouseVehicleYaw = getVehicleYaw(vehicle) state.lastManualYawApplied = 0 state.mouseOrbitActive = false state.manualSideBlend = 0 state.manualSide = 0 state.manualRearBlend = 0 state.rearPeekUntil = 0 idle.active = false idle.idleStartTick = false idle.preferredSide = nil idle.sideStats = { left = 0, right = 0 } if Config.debug then outputDebugString("[DynamicCamera] started") end end local function getTurnInput() -- v1.0.70: com cursor invisível + emulação, getControlState pode não refletir -- o A/D real quando Q/E estão segurados. Por isso lemos também as teclas -- físicas/emuladas diretamente. Isso mantém direção lateral funcionando. local left = getControlStateSafe("vehicle_left") or getControlStateSafe("steer_left") or readDriveInputDirect("vehicle_left") local right = getControlStateSafe("vehicle_right") or getControlStateSafe("steer_right") or readDriveInputDirect("vehicle_right") local value = 0 if left then value = value - 1 end if right then value = value + 1 end return value end local function getLookLeftState() -- v1.0.82: em avião/helicóptero, Q/E são controles da aeronave -- para girar/rolar no próprio eixo. Portanto não podem acionar câmera lateral. if isAirSpecialVehicle and isAirSpecialVehicle() then return false end if getControlStateSafe("vehicle_look_left") or getControlStateSafe("look_left") then return true end if Config.manualViews.useAlternativeControls then return getKeyStateSafe("q") or false end return false end local function getLookRightState() -- v1.0.82: em avião/helicóptero, Q/E são controles da aeronave -- para girar/rolar no próprio eixo. Portanto não podem acionar câmera lateral. if isAirSpecialVehicle and isAirSpecialVehicle() then return false end if getControlStateSafe("vehicle_look_right") or getControlStateSafe("look_right") then return true end if Config.manualViews.useAlternativeControls then return getKeyStateSafe("e") or false end return false end local function getLookBehindState() if getControlStateSafe("vehicle_look_behind") or getControlStateSafe("look_behind") then return true end return getLookLeftState() and getLookRightState() end local function getManualViewInput() if not Config.manualViews.enabled then return 0, false end local left = getLookLeftState() local right = getLookRightState() local behind = getLookBehindState() or state.mouse3Held or getTickCount() < (state.rearPeekUntil or 0) if behind then return 0, true end if left and not right then return -1, false elseif right and not left then return 1, false end return 0, false end releaseMouseCapture = function() -- v1.0.49: restauração agressiva. -- O cursor invisível ainda é cursor; ao sair/desativar, garantimos que ele suma -- mesmo se o estado interno tiver sido perdido. if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, 255) end if type(showCursor) == "function" then -- Se foi o nosso sistema que capturou, ou se está em modo de restauração total, escondemos sem depender do flag. if state.mouseCaptureOwned or state.mouseCaptureActive or (Config.safety and Config.safety.forceFullRestoreOnExit) then pcall(showCursor, false) end end state.mouseCaptureActive = false state.mouseCaptureOwned = false state.ignoreNextCursorMove = false end local function ensureMouseCapture() local cfg = Config.mouseOrbit if not cfg or not cfg.enabled or not cfg.captureCursor then return false end if idle.active then releaseMouseCapture(); return false end if state.manualSideBlend and state.manualSideBlend > 0.04 then releaseMouseCapture(); return false end if state.manualRearBlend and state.manualRearBlend > 0.04 then releaseMouseCapture(); return false end -- Se algum painel/menu já mostrou cursor, não roubamos o cursor. if isCursorShowing() and not state.mouseCaptureOwned then state.mouseCaptureActive = false return false end if not state.mouseCaptureOwned then showCursor(true, Config.mouseOrbit.keepControlsEnabled == true and false or true) if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, cfg.cursorAlpha or 0) end state.mouseCaptureOwned = true state.mouseCaptureActive = true local sx, sy = guiGetScreenSize() state.ignoreNextCursorMove = true setCursorPosition(sx / 2, sy / 2) else state.mouseCaptureActive = true end return true end local function updateMouseOrbit(vehicle, yaw, speed, dt, manualSideBlend, manualRearBlend) local cfg = Config.mouseOrbit if not cfg or not cfg.enabled then releaseMouseCapture() state.manualYaw = lerp(state.manualYaw or 0, 0, smoothAlpha(8.0, dt)) state.manualPitch = lerp(state.manualPitch or 0, 0, smoothAlpha(8.0, dt)) state.manualYawTarget = 0 state.manualPitchTarget = 0 state.mouseOrbitActive = false return state.manualYaw or 0, state.manualPitch or 0 end if cfg.disableOnIdle and idle.active then releaseMouseCapture() state.manualYaw = lerp(state.manualYaw or 0, 0, smoothAlpha(cfg.returnSmooth or 7.5, dt)) state.manualPitch = lerp(state.manualPitch or 0, 0, smoothAlpha(cfg.returnSmooth or 7.5, dt)) state.manualYawTarget = 0 state.manualPitchTarget = 0 state.mouseOrbitActive = false return state.manualYaw or 0, state.manualPitch or 0 end if cfg.disableWhenManualView and ((manualSideBlend or 0) > 0.04 or (manualRearBlend or 0) > 0.04) then -- v1.0.70: Q/E/Mouse3 usam a visão manual, mas NÃO podem desligar a captura do mouse. -- Antes o releaseMouseCapture() fazia a driveEmulation parar, então W/S/A/D eram limpos -- e o carro parava de acelerar ao segurar Q ou E. Mantemos a captura viva só para -- preservar a dirigibilidade emulada, zerando apenas a órbita livre do mouse. ensureMouseCapture() state.manualYaw = lerp(state.manualYaw or 0, 0, smoothAlpha((cfg.returnSmooth or 7.5) * 1.45, dt)) state.manualPitch = lerp(state.manualPitch or 0, 0, smoothAlpha((cfg.returnSmooth or 7.5) * 1.45, dt)) state.manualYawTarget = 0 state.manualPitchTarget = 0 state.mouseOrbitActive = false return state.manualYaw or 0, state.manualPitch or 0 end ensureMouseCapture() local now = getTickCount() local hasInput = (now - (state.lastMouseOrbitTick or 0)) <= (cfg.returnDelayMs or 620) -- v1.0.76: no modo 8D, o mouse tem liberdade total enquanto move, -- mas volta suavemente para a traseira quando parar de detectar movimento. if cfg.autoReturn ~= false and not hasInput then state.manualYawTarget = 0 state.manualPitchTarget = 0 end local targetYaw = clamp(state.manualYawTarget or 0, -(cfg.maxYaw or 180.0), (cfg.maxYaw or 180.0)) local targetPitch = clamp(state.manualPitchTarget or 0, cfg.maxPitchDown or -32.0, cfg.maxPitchUp or 44.0) local smooth = hasInput and (cfg.inputSmooth or 18.0) or (cfg.autoReturn == false and (cfg.inputSmooth or 18.0) or (cfg.returnSmooth or 5.8)) state.manualYaw = lerp(state.manualYaw or 0, targetYaw, smoothAlpha(smooth, dt)) state.manualPitch = lerp(state.manualPitch or 0, targetPitch, smoothAlpha(smooth, dt)) state.manualYaw = clamp(state.manualYaw or 0, -(cfg.maxYaw or 180.0), (cfg.maxYaw or 180.0)) state.manualPitch = clamp(state.manualPitch or 0, cfg.maxPitchDown or -32.0, cfg.maxPitchUp or 44.0) state.mouseOrbitActive = math.abs(state.manualYaw or 0) > 0.8 or math.abs(state.manualPitch or 0) > 0.4 return state.manualYaw or 0, state.manualPitch or 0 end local function getCameraViewModeConfig() local cfg = Config.cameraView if not cfg or not cfg.enabled or not cfg.modes or #cfg.modes == 0 then return { name = "Normal", distanceScale = 1.0, heightAdd = 0.0, lookYAdd = 0.0, fovAdd = 0 } end state.viewModeIndex = clamp(state.viewModeIndex or 1, 1, #cfg.modes) return cfg.modes[state.viewModeIndex] or cfg.modes[1] end local function cycleCameraViewMode() local cfg = Config.cameraView if not cfg or not cfg.enabled or not cfg.modes or #cfg.modes == 0 then return end state.viewModeIndex = (state.viewModeIndex or 1) + 1 if state.viewModeIndex > #cfg.modes then state.viewModeIndex = 1 end -- Evita tranco ao trocar: o novo modo nasce da câmera atual suavizada. state.entryFrom = nil local mode = getCameraViewModeConfig() if cfg.notify then outputChatBox("#ff0048[DynamicCamera] #ffffffModo de câmera: #ff0048" .. tostring(mode.name or state.viewModeIndex), 255,255,255,true) end end local function getVehicleTilt(vehicle) local rx, ry = getCachedElementRotation(vehicle) rx = math.abs(((rx or 0) + 180) % 360 - 180) ry = math.abs(((ry or 0) + 180) % 360 - 180) return math.max(rx, ry) end local function updateVehicleCameraRoll(vehicle, dt) local rf = Config.rotationFollow if not rf or not rf.enabled or not rf.cameraRollEnabled or idle.active or not isElement(vehicle) then state.cameraRoll = lerp(state.cameraRoll or 0, 0, smoothAlpha(10.0, dt or 0.016)) return state.cameraRoll or 0 end local matrix = getCachedElementMatrix(vehicle) if not matrix or not matrix[1] or not matrix[3] then state.cameraRoll = lerp(state.cameraRoll or 0, 0, smoothAlpha(rf.rollSmooth or 10.0, dt or 0.016)) return state.cameraRoll or 0 end -- right.z mostra quanto o lado direito/esquerdo subiu. -- up.z indica se o carro está normal ou de cabeça para baixo. -- atan2(right.z, up.z) gera 0 em plano, pequeno em inclinação lateral -- e chega perto de +/-180 quando o veículo capota, permitindo inversão real da câmera. local rightZ = clamp(matrix[1][3] or 0, -1, 1) local upZ = clamp(matrix[3][3] or 1, -1, 1) local rawRoll = math.deg(atan2Safe(rightZ, upZ)) if rf.invertCameraRoll then rawRoll = -rawRoll end local targetRoll if upZ <= (rf.upsideDownUpZ or -0.18) or math.abs(rawRoll) > 88 then targetRoll = clamp(rawRoll * (rf.capsizeRollInfluence or 1.0), -(rf.maxCapsizeRoll or 180.0), (rf.maxCapsizeRoll or 180.0)) else targetRoll = clamp(rawRoll * (rf.cameraRollInfluence or 0.55), -(rf.maxCameraRoll or 13.0), (rf.maxCameraRoll or 13.0)) end state.cameraRoll = lerp(state.cameraRoll or 0, targetRoll, smoothAlpha(rf.rollSmooth or 10.0, dt or 0.016)) return state.cameraRoll or 0 end local function getVehicleRelativePosition(vehicle, offsetX, offsetY, offsetZ) if not isElement(vehicle) then return nil end local matrix = getCachedElementMatrix(vehicle) if not matrix then return nil end local right, forward, up, pos = matrix[1], matrix[2], matrix[3], matrix[4] return pos[1] + right[1] * offsetX + forward[1] * offsetY + up[1] * offsetZ, pos[2] + right[2] * offsetX + forward[2] * offsetY + up[2] * offsetZ, pos[3] + right[3] * offsetX + forward[3] * offsetY + up[3] * offsetZ end local function getVehicleLookAt(vehicle, offset) offset = offset or { x = 0, y = 0, z = 0.75 } return getVehicleRelativePosition(vehicle, offset.x or 0, offset.y or 0, offset.z or 0.75) end -- ========================================================= -- IDLE CINEMATIC INTEGRADA -- ========================================================= local function idleMaybeMirrorProfile(profile) if profile.__mirrorApplied or not profile.mirrorSide then return end profile.__mirrorApplied = true local side if IdleConfig.balancedVehicleCoverage then local leftCount, rightCount = idle.sideStats.left or 0, idle.sideStats.right or 0 local tolerance = IdleConfig.sideBalanceTolerance or 1 if leftCount > rightCount + tolerance then side = 1 elseif rightCount > leftCount + tolerance then side = -1 elseif idle.preferredSide and math.random(1,100) <= (IdleConfig.sameSideChance or 45) then side = idle.preferredSide elseif idle.preferredSide then side = -idle.preferredSide else side = math.random(0,1) == 1 and 1 or -1 end elseif IdleConfig.progressiveFlow and idle.preferredSide and math.random(1,100) <= (IdleConfig.sameSideChance or 45) then side = idle.preferredSide else side = math.random(0,1) == 1 and 1 or -1 end idle.preferredSide = side if side == -1 then idle.sideStats.left = (idle.sideStats.left or 0) + 1 else idle.sideStats.right = (idle.sideStats.right or 0) + 1 end local function applySide(offset) if offset and offset.x then offset.x = side * math.abs(offset.x) end end applySide(profile.startOffset); applySide(profile.endOffset); applySide(profile.camOffset) if profile.lookOffset and profile.mirrorLookX then profile.lookOffset.x = side * math.abs(profile.lookOffset.x) end end local function idlePrepareProfile(profile) local copy = deepCopy(profile) idleMaybeMirrorProfile(copy) copy.__baseAngle = randomFloat(0, math.pi * 2) if copy.baseAngleMin and copy.baseAngleMax then copy.__baseAngle = math.rad(randomFloat(copy.baseAngleMin, copy.baseAngleMax)) elseif copy.sideBias then if math.random(0, 1) == 1 then copy.__baseAngle = math.rad(randomFloat(70, 115)) else copy.__baseAngle = math.rad(randomFloat(245, 290)) end end copy.__radius = randomFloat(copy.radiusMin or 3.5, copy.radiusMax or 5.0) copy.__height = randomFloat(copy.heightMin or 0.8, copy.heightMax or 1.3) copy.__arc = math.rad(randomFloat(copy.arcMin or 35, copy.arcMax or 75)) if math.random(0, 1) == 1 then copy.__arc = -copy.__arc end copy.__phase = randomFloat(0, math.pi * 2) return copy end local function idleChooseWeightedProfile() local totalWeight = 0 for _, p in ipairs(IdleConfig.profiles) do totalWeight = totalWeight + (p.weight or 1) end local rw, current = randomFloat(0, totalWeight), 0 for _, p in ipairs(IdleConfig.profiles) do current = current + (p.weight or 1); if rw <= current then return p end end return IdleConfig.profiles[math.random(1, #IdleConfig.profiles)] end local function idleBuildCameraForProfile(vehicle, profile, progress) if not isElement(vehicle) then return nil end progress = clamp(progress or 0, 0, 1) local eased = safeGetEasingValue(progress, "InOutQuad") local tick, t = getTickCount(), getTickCount() / 1000 local camX, camY, camZ, lookX, lookY, lookZ if profile.mode == "mega_loop" then -- Mega loops fechados: progress 0 e 1 são o mesmo ponto. -- Isso permite loop infinito sem corte seco nem ponto zero entre animações. local u = (progress % 1) * math.pi * 2 local style = profile.loopStyle or "showcase" local ox, oy, oz, lx, ly, lz if style == "low_detail_flow" then -- Loop baixo e próximo, mas sem câmera colada e sem ponto alto. -- Caminho fechado: traseira -> lateral -> frente -> lateral oposta -> traseira. ox = math.sin(u) * 4.15 + math.sin(u * 3.0) * 0.28 oy = math.cos(u) * 3.95 + math.sin(u * 2.0) * 0.32 oz = 0.74 + math.sin(u + 0.65) * 0.12 + math.sin(u * 2.0) * 0.035 lx = math.sin(u + 0.55) * 0.28 ly = math.cos(u - 0.35) * 0.36 lz = 0.66 + math.sin(u * 2.0 + 0.2) * 0.035 else -- Loop showcase lateral mais aberto, sempre baixo. -- Não há crane/drone. O máximo fica abaixo de IdleConfig.maxIdleHeight. ox = math.sin(u) * 5.55 + math.sin(u * 2.0) * 0.42 oy = math.cos(u) * 4.85 + math.cos(u * 2.0) * 0.38 oz = 1.02 + math.sin(u + 0.8) * 0.13 + math.sin(u * 2.0) * 0.04 lx = math.sin(u + 0.35) * 0.20 ly = math.cos(u - 0.25) * 0.28 lz = 0.73 + math.sin(u * 2.0) * 0.035 end -- Micro parallax temporal quase imperceptível para não parecer trilho duro. ox = ox + math.sin(t * 0.38 + profile.__phase) * 0.025 oy = oy + math.cos(t * 0.34 + profile.__phase) * 0.025 oz = oz + math.sin(t * 0.31 + profile.__phase) * 0.010 -- Trava global de altura. Isso é a correção principal da v1.0.25: -- nenhum ponto do loop pode nascer alto e depois descer. oz = clamp(oz, IdleConfig.minIdleHeight or 0.45, IdleConfig.maxIdleHeight or 1.6) lz = clamp(lz, 0.52, (IdleConfig.maxIdleHeight or 1.6) - 0.18) camX, camY, camZ = getVehicleRelativePosition(vehicle, ox, oy, oz) lookX, lookY, lookZ = getVehicleRelativePosition(vehicle, lx, ly, lz) elseif profile.mode == "orbit" then local angle = profile.__baseAngle + profile.__arc * eased local radius = profile.__radius * (profile.distanceScale or IdleConfig.orbitDistanceScale or 1) local height = profile.__height local micro = math.sin(t * 0.45 + profile.__phase) * 0.08 local ox = math.cos(angle) * (radius + micro) local oy = math.sin(angle) * (radius + micro) local oz = height + math.sin(t * 0.35 + profile.__phase) * 0.04 camX, camY, camZ = getVehicleRelativePosition(vehicle, ox, oy, oz) lookX, lookY, lookZ = getVehicleLookAt(vehicle, profile.lookOffset) elseif profile.mode == "slider" or profile.mode == "push" then local s, e = profile.startOffset, profile.endOffset local ox = s.x + (e.x - s.x) * eased local oy = s.y + (e.y - s.y) * eased local oz = s.z + (e.z - s.z) * eased local distanceScale = profile.distanceScale or IdleConfig.cameraDistanceScale or 1 ox, oy = ox * distanceScale, oy * distanceScale ox = ox + math.sin(t * 0.55 + profile.__phase) * 0.035 oy = oy + math.cos(t * 0.45 + profile.__phase) * 0.035 camX, camY, camZ = getVehicleRelativePosition(vehicle, ox, oy, oz) lookX, lookY, lookZ = getVehicleLookAt(vehicle, profile.lookOffset) elseif profile.mode == "detail" then local o = profile.camOffset local ox = o.x + math.sin(t * 0.9 + profile.__phase) * (profile.swayX or 0.04) local oy = o.y + math.cos(t * 0.75 + profile.__phase) * (profile.swayY or 0.04) local oz = o.z + math.sin(t * 1.15 + profile.__phase) * (profile.swayZ or 0.02) local distanceScale = profile.distanceScale or IdleConfig.detailDistanceScale or 1 ox, oy = ox * distanceScale, oy * distanceScale camX, camY, camZ = getVehicleRelativePosition(vehicle, ox, oy, oz) lookX, lookY, lookZ = getVehicleLookAt(vehicle, profile.lookOffset) end if not camX or not lookX then return nil end local shake = profile.shake or 0 if shake > 0 then camX = camX + math.sin(t * 6.2 + profile.__phase) * shake camY = camY + math.cos(t * 5.7 + profile.__phase) * shake camZ = camZ + math.sin(t * 7.1 + profile.__phase) * shake * 0.45 lookX = lookX + math.sin(t * 2.2 + profile.__phase) * shake * 0.25 lookY = lookY + math.cos(t * 2.0 + profile.__phase) * shake * 0.25 lookZ = lookZ + math.sin(t * 2.6 + profile.__phase) * shake * 0.15 end return { cam = { camX, camY, camZ }, look = { lookX, lookY, lookZ } } end local function idleDistanceBetweenCameraPoints(a, b) if not a or not b or not a.cam or not b.cam then return 999999 end local dx = (a.cam[1] or 0) - (b.cam[1] or 0) local dy = (a.cam[2] or 0) - (b.cam[2] or 0) local dz = ((a.cam[3] or 0) - (b.cam[3] or 0)) * (IdleConfig.heightPenaltyInSearch or 1) local lx, ly, lz = 0, 0, 0 if a.look and b.look then lx = ((a.look[1] or 0) - (b.look[1] or 0)) * 0.35 ly = ((a.look[2] or 0) - (b.look[2] or 0)) * 0.35 lz = ((a.look[3] or 0) - (b.look[3] or 0)) * 0.35 end return dx * dx + dy * dy + dz * dz + lx * lx + ly * ly + lz * lz end local function idleReversePreparedProfile(profile) if not profile then return end if (profile.mode == "slider" or profile.mode == "push") and profile.startOffset and profile.endOffset then local oldStart = profile.startOffset profile.startOffset = profile.endOffset profile.endOffset = oldStart elseif profile.mode == "orbit" then profile.__baseAngle = (profile.__baseAngle or 0) + (profile.__arc or 0) profile.__arc = -(profile.__arc or 0) end end local function idleFindClosestProgress(vehicle, profile, fromCam, fromLook) if not isElement(vehicle) or not fromCam or not fromLook then return 0 end -- Detail é praticamente uma câmera parada com leve sway; não compensa buscar ponto interno. if profile.mode == "detail" then return 0 end local reference = { cam = { fromCam[1], fromCam[2], fromCam[3] }, look = { fromLook[1], fromLook[2], fromLook[3] } } local samples = math.max(6, profile.mode == "mega_loop" and (IdleConfig.megaLoopStartSearchSamples or 48) or (IdleConfig.profileStartSearchSamples or 16)) local bestP, bestDistance = 0, 999999 for i = 0, samples do local p = i / samples local candidate = idleBuildCameraForProfile(vehicle, profile, p) local d = idleDistanceBetweenCameraPoints(reference, candidate) if d < bestDistance then bestDistance = d bestP = p end end -- Se o ponto mais próximo está no final da animação, inverte o caminho. -- Assim a próxima animação não precisa "voltar para o começo" nem nascer de cima. if IdleConfig.reverseProfileIfCloserToEnd and bestP > 0.62 and profile.mode ~= "detail" and profile.mode ~= "mega_loop" then idleReversePreparedProfile(profile) bestP = 1 - bestP end -- Em mega_loop podemos entrar em qualquer ponto, porque o caminho é fechado e infinito. if profile.mode ~= "mega_loop" then -- Evita entrar no último pedaço do perfil, que causaria troca muito rápida. local maxStart = 1 - (IdleConfig.minRemainingProfileProgress or 0.42) bestP = clamp(bestP, 0, maxStart) end return bestP end local function idleChooseNewProfile(fromCam, fromLook) if not idle.active then return false end local vehicle = state.vehicle if not isElement(vehicle) then stopDynamicCamera("idle_vehicle_destroyed"); return false end -- Ponto mais importante da v1.0.22: -- a próxima animação sempre nasce da última câmera REAL renderizada. -- Isso evita o bug de voltar para a chase camera / ponto zero por 1 frame -- quando troca de perfil cinematográfico. if fromCam and fromLook then idle.fromCam = { fromCam[1], fromCam[2], fromCam[3] } idle.fromLook = { fromLook[1], fromLook[2], fromLook[3] } elseif idle.lastCam and idle.lastLook then idle.fromCam = { idle.lastCam[1], idle.lastCam[2], idle.lastCam[3] } idle.fromLook = { idle.lastLook[1], idle.lastLook[2], idle.lastLook[3] } elseif state.currentCam and state.currentLook then idle.fromCam = { state.currentCam[1], state.currentCam[2], state.currentCam[3] } idle.fromLook = { state.currentLook[1], state.currentLook[2], state.currentLook[3] } else local cx, cy, cz, lx, ly, lz = getCameraMatrix() idle.fromCam = { cx, cy, cz } idle.fromLook = { lx, ly, lz } end local selected = idleChooseWeightedProfile() local profile = idlePrepareProfile(selected) -- v1.0.23: continuidade real. -- A nova animação não começa obrigatoriamente no progress 0. -- Ela procura, dentro do próprio caminho, o ponto mais próximo da câmera atual -- e passa a tocar a partir dali. Isso elimina a sensação de nascer de cima/ponto zero. profile.__startProgress = idleFindClosestProgress(vehicle, profile, idle.fromCam, idle.fromLook) idle.currentProfile = profile idle.lastProfileName = profile.name idle.profileStartTick = getTickCount() idle.profileDuration = math.random(profile.durationMin or 5000, profile.durationMax or 7500) idle.transitionStartTick = getTickCount() idle.transitionDuration = math.random(IdleConfig.smoothTransitionMin or IdleConfig.transitionTime, IdleConfig.smoothTransitionMax or IdleConfig.transitionTime) return true end local function idleStart() if idle.active then return end if releaseMouseCapture then releaseMouseCapture() end local vehicle = state.vehicle if not IdleConfig.enabled or not isElement(vehicle) then return end idle.active = true idle.currentProfile = nil idle.lastCam = state.currentCam and { state.currentCam[1], state.currentCam[2], state.currentCam[3] } or nil idle.lastLook = state.currentLook and { state.currentLook[1], state.currentLook[2], state.currentLook[3] } or nil idle.preferredSide = nil idle.sideStats = { left = 0, right = 0 } if IdleConfig.forceVehicleStreamable and setElementStreamable then setElementStreamable(vehicle, true) end idleChooseNewProfile() -- FOV volta para padrão durante idle de forma suave pelo render principal. end local function idleExit(reason) if not idle.active then return end idle.active = false idle.lastCloseTick = getTickCount() idle.idleStartTick = false idle.currentProfile = nil idle.fromCam = nil idle.fromLook = nil idle.lastCam = nil idle.lastLook = nil -- Não chama setCameraTarget aqui. A câmera chase assume suavemente a posição atual. if Config.debug then outputDebugString("[DynamicCamera] idle exit: " .. tostring(reason)) end end local function idleUpdateTrigger(vehicle, speed) local specialProfile = getCurrentSpecialProfile and getCurrentSpecialProfile() or nil if specialProfile and specialProfile.disableIdle then idle.idleStartTick = false; return end 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() and not state.mouseCaptureActive then idle.idleStartTick = false; return end if state.mouse3Held or getLookLeftState() or getLookRightState() then idle.idleStartTick = false; return end if speed <= IdleConfig.minSpeedToCancel 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 idleRender(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.minSpeedToCancel or state.mouse3Held or getLookLeftState() or getLookRightState() then idleExit("input_or_moved") return nil end if not idle.currentProfile then if not idleChooseNewProfile() then return nil end end local now = getTickCount() local elapsed = now - idle.profileStartTick -- Não retorna nil na troca de animação. -- Antes isso fazia o render principal cair na chase camera por 1 frame, -- parecendo que a câmera voltava para o ponto zero a cada corte. if elapsed >= idle.profileDuration then if idle.currentProfile and idle.currentProfile.mode == "mega_loop" and IdleConfig.megaLoopLocked then -- Não troca de animação: mantém o loop matemático rodando. -- Isso elimina o corte ou nascimento em ponto alto/ponto zero. elapsed = elapsed % idle.profileDuration idle.profileStartTick = now - elapsed else local bridgeCam = idle.lastCam or state.currentCam local bridgeLook = idle.lastLook or state.currentLook if not idleChooseNewProfile(bridgeCam, bridgeLook) then return nil end now = getTickCount() elapsed = 0 end end local progress = clamp(elapsed / idle.profileDuration, 0, 1) local startProgress = idle.currentProfile.__startProgress or 0 local effectiveProgress if idle.currentProfile.mode == "mega_loop" then -- Loop fechado: começa no ponto mais próximo da câmera atual e segue circularmente. effectiveProgress = (startProgress + progress) % 1 else effectiveProgress = startProgress + (1 - startProgress) * progress effectiveProgress = clamp(effectiveProgress, 0, 1) end local target = idleBuildCameraForProfile(vehicle, idle.currentProfile, effectiveProgress) if not target then idleExit("invalid_idle_target"); return nil end local transitionProgress = clamp((now - idle.transitionStartTick) / idle.transitionDuration, 0, 1) local e = safeGetEasingValue(transitionProgress, "InOutQuad") local result if transitionProgress < 1 and idle.fromCam and idle.fromLook then local camX, camY, camZ = interpolateBetween(idle.fromCam[1], idle.fromCam[2], idle.fromCam[3], target.cam[1], target.cam[2], target.cam[3], e, "InOutQuad") local lookX, lookY, lookZ = interpolateBetween(idle.fromLook[1], idle.fromLook[2], idle.fromLook[3], target.look[1], target.look[2], target.look[3], e, "InOutQuad") result = { cam = { camX, camY, camZ }, look = { lookX, lookY, lookZ } } else result = target end idle.lastCam = { result.cam[1], result.cam[2], result.cam[3] } idle.lastLook = { result.look[1], result.look[2], result.look[3] } return result end local function resetGForce(dt) local a = smoothAlpha((Config.gForce and Config.gForce.resetSmooth) or 8.5, dt or 0.016) state.gForceY = lerp(state.gForceY or 0, 0, a) state.gForceSide = lerp(state.gForceSide or 0, 0, a) state.gForceLookSide = lerp(state.gForceLookSide or 0, 0, a) state.gForceLookZ = lerp(state.gForceLookZ or 0, 0, a) state.lastForwardSpeed = nil state.lastSideSpeed = nil return state.gForceY or 0, state.gForceSide or 0, state.gForceLookSide or 0, state.gForceLookZ or 0 end local function updateGForceCamera(vehicle, yaw, speed, dt, effectsAllowed) local cfg = Config.gForce if not cfg or not cfg.enabled or not effectsAllowed or not isElement(vehicle) or speed < (cfg.minSpeed or 6.0) then return resetGForce(dt) end local vx, vy = getCachedElementVelocity(vehicle) local r = math.rad(yaw or 0) local rightX, rightY = math.cos(r), math.sin(r) local forwardX, forwardY = -math.sin(r), math.cos(r) -- velocidades locais em km/h no eixo do carro local forwardSpeed = (vx * forwardX + vy * forwardY) * 180 local sideSpeed = (vx * rightX + vy * rightY) * 180 local forwardAccel, sideAccel = 0, 0 if state.lastForwardSpeed ~= nil then forwardAccel = (forwardSpeed - state.lastForwardSpeed) / math.max(dt, 0.001) end if state.lastSideSpeed ~= nil then sideAccel = (sideSpeed - state.lastSideSpeed) / math.max(dt, 0.001) end state.lastForwardSpeed = forwardSpeed state.lastSideSpeed = sideSpeed local dead = cfg.deadzone or 4.0 local yTarget = 0 if forwardAccel > dead then yTarget = -clamp((forwardAccel - dead) * (cfg.accelPullScale or 0.0105), 0, cfg.maxBackwardPull or 0.52) elseif forwardAccel < -dead then yTarget = clamp((-forwardAccel - dead) * (cfg.brakePushScale or 0.0090), 0, cfg.maxForwardPush or 0.40) end -- sideAccel positivo = peso visual para o lado oposto, mas sem virar brusco. local camSideTarget = clamp(-sideAccel * (cfg.lateralCamScale or 0.0058), -(cfg.maxSideOffset or 0.24), cfg.maxSideOffset or 0.24) local lookSideTarget = clamp(sideAccel * (cfg.lateralLookScale or 0.0095), -(cfg.maxLookSide or 0.42), cfg.maxLookSide or 0.42) local lookZTarget = clamp(math.abs(forwardAccel) * (cfg.pitchLookScale or 0.0016), 0, cfg.maxPitchLook or 0.13) local a = smoothAlpha(cfg.smooth or 10.5, dt) state.gForceY = lerp(state.gForceY or 0, yTarget, a) state.gForceSide = lerp(state.gForceSide or 0, camSideTarget, a) state.gForceLookSide = lerp(state.gForceLookSide or 0, lookSideTarget, a) state.gForceLookZ = lerp(state.gForceLookZ or 0, lookZTarget, a) return state.gForceY or 0, state.gForceSide or 0, state.gForceLookSide or 0, state.gForceLookZ or 0 end -- ========================================================= -- CHASE CAMERA -- ========================================================= local function buildTargetCamera(vehicle, dt) if not isElement(vehicle) then return nil end local speed = getSpeedKMH(vehicle) local yaw = getVehicleYaw(vehicle) local signedSpeed = getSignedForwardSpeedKMH(vehicle, yaw) local tilt = getVehicleTilt(vehicle) local effectsAllowed = tilt <= Config.safety.maxTiltForEffects if state.lastYaw then local diff = angleDiff(yaw, state.lastYaw) local rawRate = diff / math.max(dt, 0.001) state.yawRate = lerp(state.yawRate or 0, rawRate, smoothAlpha(7.0, dt)) end state.lastYaw = yaw local reverseTarget = signedSpeed < -2.5 and 1 or 0 state.reverseBlend = lerp(state.reverseBlend, reverseTarget, smoothAlpha(Config.camera.reverseSmooth, dt)) local manualSide, manualRear = getManualViewInput() local sideTarget = manualRear and 0 or manualSide local rearTarget = manualRear and 1 or 0 state.manualSide = lerp(state.manualSide or 0, sideTarget, smoothAlpha(Config.manualViews.sideSmooth, dt)) state.manualSideBlend = lerp(state.manualSideBlend or 0, math.abs(sideTarget), smoothAlpha(Config.manualViews.sideSmooth, dt)) state.manualRearBlend = lerp(state.manualRearBlend or 0, rearTarget, smoothAlpha(Config.manualViews.rearSmooth, dt)) local mouseYaw, mousePitch = updateMouseOrbit(vehicle, yaw, speed, dt, state.manualSideBlend, state.manualRearBlend) local mouseOrbitFactor = clamp(math.abs(mouseYaw or 0) / math.max((Config.mouseOrbit and Config.mouseOrbit.maxYaw) or 92.0, 1), 0, 1) local turnVisionReduction = 1.0 - mouseOrbitFactor * ((Config.mouseOrbit and Config.mouseOrbit.reduceTurnVision) or 0.0) -- v1.0.77: força G real por eixo local do veículo. -- Mantém compatibilidade com Config.camera.accelPullMax/brakePushMax via state.accelEffect, -- mas agora a origem é aceleração longitudinal/lateral, não apenas speed global. state.lastSpeed = speed local gY, gSide, gLookSide, gLookZ = updateGForceCamera(vehicle, yaw, speed, dt, effectsAllowed) state.accelEffect = gY or 0 local turnTarget = 0 if Config.turnVision.enabled and effectsAllowed and speed >= Config.turnVision.minSpeed then local sf = clamp((speed - Config.turnVision.minSpeed) / math.max(1, Config.turnVision.fullSpeed - Config.turnVision.minSpeed), 0, 1) local input = getTurnInput() * Config.turnVision.inputStrength local yawPart = clamp((state.yawRate or 0) / 55, -1, 1) * Config.turnVision.yawRateStrength turnTarget = clamp((input + yawPart) * sf * turnVisionReduction, -1, 1) if Config.turnVision.invert then turnTarget = -turnTarget end end state.turnAmount = lerp(state.turnAmount, turnTarget, smoothAlpha(Config.turnVision.smooth, dt)) local shakeTarget = 0 if Config.cornerShake.enabled and effectsAllowed and speed >= Config.cornerShake.minSpeed then local sf = clamp((speed - Config.cornerShake.minSpeed) / math.max(1, Config.cornerShake.fullSpeed - Config.cornerShake.minSpeed), 0, 1) shakeTarget = (math.abs(state.turnAmount) ^ Config.cornerShake.curvePower) * sf end state.shakeAmount = lerp(state.shakeAmount, shakeTarget, smoothAlpha(Config.cornerShake.smooth, dt)) local viewMode = getCameraViewModeConfig() local fovDefault = Config.fov.default + ((viewMode and viewMode.fovAdd) or 0) local fovMax = Config.fov.max + ((viewMode and viewMode.fovAdd) or 0) local targetFov = fovDefault if Config.fov.enabled and effectsAllowed and not idle.active then targetFov = lerp(fovDefault, fovMax, clamp(speed / Config.fov.speedForMax, 0, 1)) end setFovSafe(lerp(state.currentFov or fovDefault, targetFov, smoothAlpha(Config.fov.smooth, dt))) local t, reverse = state.turnAmount or 0, state.reverseBlend or 0 local forwardCam, forwardLook = Config.camera.forwardCamOffset, Config.camera.forwardLookOffset local reverseCam, reverseLook = Config.camera.reverseCamOffset, Config.camera.reverseLookOffset local camSide = clamp(-t * Config.turnVision.cameraSideShift + (gSide or 0), -Config.turnVision.maxCameraSide - 0.24, Config.turnVision.maxCameraSide + 0.24) local lookSide = clamp(t * Config.turnVision.lookSideShift + (gLookSide or 0), -Config.turnVision.maxLookSide - 0.42, Config.turnVision.maxLookSide + 0.42) local frontBoost = clamp(math.abs(t) * Config.turnVision.frontLookBoost, 0, Config.turnVision.maxFrontBoost) local camForward = { x = forwardCam.x + camSide, y = forwardCam.y + state.accelEffect, z = forwardCam.z } local lookForward = { x = forwardLook.x + lookSide, y = forwardLook.y + frontBoost, z = forwardLook.z + (gLookZ or 0) } local reverseT = t * 0.35 local camReverse = { x = reverseCam.x + clamp(reverseT * 0.20, -0.22, 0.22), y = reverseCam.y, z = reverseCam.z } local lookReverse = { x = reverseLook.x + clamp(reverseT * 0.30, -0.32, 0.32), y = reverseLook.y, z = reverseLook.z } if viewMode then local ds = viewMode.distanceScale or 1.0 local h = viewMode.heightAdd or 0.0 local lyAdd = viewMode.lookYAdd or 0.0 camForward.y = camForward.y * ds camReverse.y = camReverse.y * ds camForward.z = camForward.z + h camReverse.z = camReverse.z + h lookForward.y = lookForward.y + lyAdd lookReverse.y = lookReverse.y - lyAdd end local ox, oy, oz = lerp(camForward.x, camReverse.x, reverse), lerp(camForward.y, camReverse.y, reverse), lerp(camForward.z, camReverse.z, reverse) local lx, ly, lz = lerp(lookForward.x, lookReverse.x, reverse), lerp(lookForward.y, lookReverse.y, reverse), lerp(lookForward.z, lookReverse.z, reverse) local sideBlend = state.manualSideBlend or 0 if sideBlend > 0.001 then local sideSign = (state.manualSide or 0) >= 0 and 1 or -1 local sideCam, sideLook = Config.manualViews.sideCam, Config.manualViews.sideLook local ds = (viewMode and viewMode.distanceScale) or 1.0 local h = (viewMode and viewMode.heightAdd) or 0.0 ox = lerp(ox, sideCam.x * sideSign * ds, sideBlend); oy = lerp(oy, sideCam.y * ds, sideBlend); oz = lerp(oz, sideCam.z + h, sideBlend) lx = lerp(lx, sideLook.x * sideSign, sideBlend); ly = lerp(ly, sideLook.y, sideBlend); lz = lerp(lz, sideLook.z, sideBlend) end local rearBlend = state.manualRearBlend or 0 if rearBlend > 0.001 then local rearCam, rearLook = Config.manualViews.rearCamOffset, Config.manualViews.rearLookOffset local ds = (viewMode and viewMode.distanceScale) or 1.0 local h = (viewMode and viewMode.heightAdd) or 0.0 ox = lerp(ox, rearCam.x, rearBlend); oy = lerp(oy, rearCam.y * ds, rearBlend); oz = lerp(oz, rearCam.z + h, rearBlend) lx = lerp(lx, rearLook.x, rearBlend); ly = lerp(ly, rearLook.y, rearBlend); lz = lerp(lz, rearLook.z, rearBlend) end -- v1.0.75: MouseOrbit 8D. -- Antes o mouse girava só a posição da câmera, mas o look continuava preso no yaw do carro. -- Agora a câmera e o foco podem acompanhar o ângulo do mouse, permitindo ver frente, traseira, laterais, cima e baixo. local orbitYaw = yaw + (mouseYaw or 0) local lookYaw = yaw local mo = Config.mouseOrbit if mo and mo.lookWithOrbit and (state.mouseOrbitActive or math.abs(mouseYaw or 0) > 0.25 or math.abs(mousePitch or 0) > 0.25) then lookYaw = orbitYaw end local pitch = mousePitch or 0 local pitchNorm = 0 if pitch >= 0 then pitchNorm = pitch / math.max(1, (mo and mo.maxPitchUp) or 44.0) else pitchNorm = pitch / math.max(1, math.abs((mo and mo.maxPitchDown) or -32.0)) end pitchNorm = clamp(pitchNorm, -1, 1) -- Pitch livre: move principalmente o ponto de foco, com micro ajuste na altura da câmera. -- Isso dá sensação 8D sem jogar a câmera para posições absurdas. local pitchLookAdd = pitchNorm * ((mo and mo.pitchLookInfluence) or 2.70) local pitchCamAdd = -pitchNorm * ((mo and mo.pitchCameraInfluence) or 0.28) local camX, camY, camZ = localToWorldYaw(vehicle, ox, oy, oz + pitchCamAdd, orbitYaw) local lookX, lookY, lookZ = localToWorldYaw(vehicle, lx, ly, lz + pitchLookAdd, lookYaw, true) if not camX or not lookX then return nil end if state.shakeAmount > 0.001 and not idle.active then local now, s, freq = getTickCount() / 1000, state.shakeAmount, Config.cornerShake.frequency local posAmp, lookAmp = Config.cornerShake.maxPosition * s, Config.cornerShake.maxLook * s camX = camX + math.sin(now * freq * 1.31) * posAmp camY = camY + math.cos(now * freq * 1.07) * posAmp camZ = camZ + math.sin(now * freq * 1.63) * posAmp * 0.45 lookX = lookX + math.sin(now * freq * 0.91) * lookAmp lookY = lookY + math.cos(now * freq * 0.87) * lookAmp end -- Shake de asfalto/suspensão: leve no piso normal, mais evidente quando a suspensão toma impacto. -- A v1.0.75 usa velocidade vertical + aceleração vertical + mudança rápida de pitch/roll. -- Isso faz trilho de trem, guia, buraco e lombada aparecerem melhor na tela. if Config.roadShake and Config.roadShake.enabled and effectsAllowed and not idle.active and speed >= (Config.roadShake.minSpeed or 8.0) then local rs = Config.roadShake local speedFactor = clamp((speed - (rs.minSpeed or 8.0)) / math.max(1, (rs.fullSpeed or 115.0) - (rs.minSpeed or 8.0)), 0, 1) local _, _, vz = getCachedElementVelocity(vehicle) vz = vz or 0 local rv = math.abs(vz) * 28.0 local verticalVelocityFactor = clamp(rv, 0, 1) * (rs.verticalVelocityInfluence or 0.44) local lastVz = state.lastRoadVz local verticalAccelFactor = 0 if lastVz ~= nil then local dvz = math.abs(vz - lastVz) / math.max(dt, 0.001) -- Trilho/lombada costuma gerar pico curto aqui; abaixo do threshold quase não vibra. local threshold = rs.impactThreshold or 0.028 verticalAccelFactor = clamp((dvz - threshold) * 2.8, 0, 1) * (rs.verticalAccelInfluence or 0.72) end state.lastRoadVz = vz local rx, ry = getCachedElementRotation(vehicle) rx, ry = rx or 0, ry or 0 local rotationImpact = 0 if state.lastRoadRx ~= nil and state.lastRoadRy ~= nil then local drx = math.abs(angleDiff(rx, state.lastRoadRx)) / math.max(dt, 0.001) local dry = math.abs(angleDiff(ry, state.lastRoadRy)) / math.max(dt, 0.001) rotationImpact = clamp((drx + dry) / 75.0, 0, 1) * (rs.rotationImpactInfluence or 0.36) end state.lastRoadRx, state.lastRoadRy = rx, ry local impulse = clamp(verticalVelocityFactor + verticalAccelFactor + rotationImpact, 0, 1) state.roadShakeImpact = math.max(state.roadShakeImpact or 0, impulse) state.roadShakeImpact = lerp(state.roadShakeImpact or 0, 0, smoothAlpha(rs.impactDecay or 10.0, dt)) local baseAmount = clamp(speedFactor * (rs.speedInfluence or 0.42) + verticalVelocityFactor * 0.45, 0, 1) local impactAmount = clamp(state.roadShakeImpact or 0, 0, 1) local amount = clamp(baseAmount + impactAmount * 1.10, 0, 1) if amount > 0.001 then local now = getTickCount() / 1000 local baseFreq = rs.frequency or 10.8 local impactFreq = rs.impactFrequency or 18.0 local posAmp = (rs.maxPosition or 0.0095) * baseAmount + (rs.impactPosition or 0.021) * impactAmount local lookAmp = (rs.maxLook or 0.0048) * baseAmount + (rs.impactLook or 0.010) * impactAmount -- Base: vibração pequena de asfalto. camX = camX + math.sin(now * baseFreq * 1.73) * posAmp * 0.22 camY = camY + math.cos(now * baseFreq * 1.41) * posAmp * 0.18 camZ = camZ + math.sin(now * baseFreq * 2.07) * posAmp * 0.52 -- Impacto: pulso mais rápido vertical, sentido em trilho/lombada sem exagerar laterais. camZ = camZ + math.sin(now * impactFreq * 2.35) * (rs.impactPosition or 0.021) * impactAmount * 0.88 lookZ = lookZ + math.cos(now * impactFreq * 1.85) * (rs.impactLook or 0.010) * impactAmount * 0.62 lookX = lookX + math.sin(now * baseFreq * 1.12) * lookAmp * 0.20 lookY = lookY + math.cos(now * baseFreq * 1.26) * lookAmp * 0.18 lookZ = lookZ + math.sin(now * baseFreq * 1.58) * lookAmp * 0.30 end else state.roadShakeImpact = lerp(state.roadShakeImpact or 0, 0, smoothAlpha(12.0, dt or 0.016)) state.lastRoadVz = nil state.lastRoadRx = nil state.lastRoadRy = nil end return { cam = { camX, camY, camZ }, look = { lookX, lookY, lookZ } } end local function disableCustomCameraForPassenger(reason) applyVehicleSpecialRuntimeConfig(nil) -- v1.0.57: -- Passageiro deve ficar 100% nativo do MTA. -- Esta função só limpa resíduos caso o sistema estivesse ativo antes; -- depois disso, não aplica setCameraMatrix, não força V/Q/E/Mouse3 e não fica restaurando todo frame. if releaseMouseCapture then releaseMouseCapture() end clearDriveEmulation() clearDriveKeyHeldStates() state.active = false state.vehicle = nil state.currentCam = nil state.currentLook = nil state.entryFrom = nil state.reverseBlend = 0 state.manualSideBlend = 0 state.manualSide = 0 state.manualRearBlend = 0 state.rearPeekUntil = 0 state.mouse3Held = false state.accelEffect = 0 state.gForceY = 0 state.gForceSide = 0 state.gForceLookSide = 0 state.gForceLookZ = 0 state.lastForwardSpeed = nil state.lastSideSpeed = nil state.turnAmount = 0 state.shakeAmount = 0 state.cameraRoll = 0 state.roadShakeImpact = 0 state.lastRoadVz = nil state.lastRoadRx = nil state.lastRoadRy = nil state.lastYaw = nil state.yawRate = 0 state.manualYaw = 0 state.manualYawTarget = 0 state.manualPitch = 0 state.manualPitchTarget = 0 state.lastMouseOrbitTick = 0 state.lastPedCamRot = nil state.lastMouseVehicleYaw = nil state.lastManualYawApplied = 0 state.mouseOrbitActive = false state.pendingVehicleExit = false state.enterExitPulseUntil = 0 idle.active = false idle.idleStartTick = false idle.currentProfile = nil idle.fromCam = nil idle.fromLook = nil setFovSafe(Config.fov.default) setCameraTarget(localPlayer) if Config.debug then outputDebugString("[DynamicCamera] passenger native mode: " .. tostring(reason)) end end local function renderDynamicCamera(timeSlice) -- v1.0.63: prioridade máxima durante a animação de saída. -- O MTA pode demorar para atualizar getPedOccupiedVehicle(), então não esperamos isso. if renderExitDetachCamera() then return end -- v1.0.49: janela de restauração nativa. Durante esse tempo o script -- NÃO chama setCameraMatrix e NÃO reinicia a câmera, evitando câmera presa ao desembarcar. if (state.nativeRestoreUntil or 0) > getTickCount() then -- v1.0.61: janela curtíssima apenas para impedir setCameraMatrix no mesmo frame da saída. -- Se o player já saiu do veículo, libera a janela NA HORA para não deixar a câmera -- presa no carro por 1~2 segundos. local nowLock = getTickCount() if releaseMouseCapture then releaseMouseCapture() end if type(setCursorAlpha) == "function" then pcall(setCursorAlpha, 255) end if type(showCursor) == "function" then pcall(showCursor, false) end if type(guiSetInputEnabled) == "function" then pcall(guiSetInputEnabled, false) end if type(toggleAllControls) == "function" then pcall(toggleAllControls, true, true, true) end setFovSafe(Config.fov.default) setCameraTarget(localPlayer) local stillInVehicle = getPedOccupiedVehicle(localPlayer) if not stillInVehicle then state.nativeRestoreUntil = 0 state.exitLockUntil = 0 state.pendingVehicleExit = false state.enterExitPulseUntil = 0 if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, "enter_exit", false) end kickCameraToPedAfterExit("native_window_no_vehicle") return end if (state.enterExitPulseUntil or 0) > nowLock then if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, "enter_exit", true) end else if type(setPedControlState) == "function" then pcall(setPedControlState, localPlayer, "enter_exit", false) end end return end if not Config.enabled then if state.active then stopDynamicCamera("disabled") end return end -- v1.0.57: trava principal de motorista/passageiro. -- Se o player estiver como passageiro, nenhuma câmera custom, MouseOrbit, idle, V ou emulação deve rodar. local occupiedVehicle, occupiedSeat = getLocalOccupiedVehicleAndSeat() if not occupiedVehicle then applyVehicleSpecialRuntimeConfig(nil) if state.active or idle.active or state.mouseCaptureActive or state.driveEmulationActive then stopDynamicCamera("no_vehicle") end return end if Config.activation.driverSeatOnly ~= false and occupiedSeat ~= 0 then -- v1.0.57: Passageiro = MTA nativo. -- Não renderiza câmera, não captura mouse, não emula controles, não reage a V/Q/E/Mouse3. -- Só limpa resíduos caso o player tenha acabado de sair do banco do motorista. if state.active or idle.active or state.mouseCaptureActive or state.driveEmulationActive or state.currentCam or state.currentLook then disableCustomCameraForPassenger("passenger_seat_" .. tostring(occupiedSeat)) end return end local vehicle = occupiedVehicle if not state.active then if Config.activation.autoStartWhenDriver then startDynamicCamera(vehicle) else return end end if state.vehicle ~= vehicle then stopDynamicCamera("vehicle_changed"); startDynamicCamera(vehicle); return end if Config.activation.disableOnFirstPerson and getCameraViewMode and getCameraViewMode() == 0 then stopDynamicCamera("first_person"); return end applyVehicleSpecialRuntimeConfig(vehicle) local now = getTickCount() local dt = (timeSlice or (now - state.lastTick)) / 1000 dt = clamp(dt, 0.001, 0.08) state.lastTick = now local speed = getSpeedKMH(vehicle) idleUpdateTrigger(vehicle, speed) local target = nil if idle.active then target = idleRender(vehicle, dt) end if not target then target = buildTargetCamera(vehicle, dt) end if not target then stopDynamicCamera("invalid_target"); return end local entryP = 1 if state.entryFrom then entryP = clamp((now - state.entryStartTick) / Config.camera.entryBlendMs, 0, 1) entryP = entryP * entryP * (3 - 2 * entryP) end if not state.currentCam then if state.entryFrom then state.currentCam = { state.entryFrom.cam[1], state.entryFrom.cam[2], state.entryFrom.cam[3] } state.currentLook = { state.entryFrom.look[1], state.entryFrom.look[2], state.entryFrom.look[3] } else state.currentCam = { target.cam[1], target.cam[2], target.cam[3] } state.currentLook = { target.look[1], target.look[2], target.look[3] } end end local posSmooth = idle.active and 999 or Config.camera.positionSmooth local lookSmooth = idle.active and 999 or Config.camera.lookSmooth local posA = smoothAlpha(posSmooth, dt) local lookA = smoothAlpha(lookSmooth, dt) state.currentCam[1] = lerp(state.currentCam[1], target.cam[1], posA) state.currentCam[2] = lerp(state.currentCam[2], target.cam[2], posA) state.currentCam[3] = lerp(state.currentCam[3], target.cam[3], posA) state.currentLook[1] = lerp(state.currentLook[1], target.look[1], lookA) state.currentLook[2] = lerp(state.currentLook[2], target.look[2], lookA) state.currentLook[3] = lerp(state.currentLook[3], target.look[3], lookA) local camX, camY, camZ = state.currentCam[1], state.currentCam[2], state.currentCam[3] local lookX, lookY, lookZ = state.currentLook[1], state.currentLook[2], state.currentLook[3] if state.entryFrom and entryP < 1 then camX = lerp(state.entryFrom.cam[1], camX, entryP); camY = lerp(state.entryFrom.cam[2], camY, entryP); camZ = lerp(state.entryFrom.cam[3], camZ, entryP) lookX = lerp(state.entryFrom.look[1], lookX, entryP); lookY = lerp(state.entryFrom.look[2], lookY, entryP); lookZ = lerp(state.entryFrom.look[3], lookZ, entryP) end if not isElement(vehicle) then stopDynamicCamera("vehicle_destroyed"); return end updateDriveEmulation(vehicle) local finalRoll = idle.active and 0 or updateVehicleCameraRoll(vehicle, dt) setCameraMatrix(camX, camY, camZ, lookX, lookY, lookZ, finalRoll or 0, state.currentFov or Config.fov.default) end registerDriveKeyStateBinds() addEventHandler("onClientPreRender", root, renderDynamicCamera) -- v1.0.59: se o jogador começar a sair/pular do veículo ainda em movimento, -- restauramos a câmera/mouse/controles no início da saída, não só depois do onClientVehicleExit. addEventHandler("onClientVehicleStartExit", root, function(player, seat) if player ~= localPlayer then return end -- v1.0.65: bloqueia a saída em movimento para motorista e passageiros. -- Se for passageiro e o carro estiver parado, não faz mais nada: deixa o MTA nativo cuidar. if blockVehicleExitNow("vehicle_start_exit", source) then cancelEvent() return end -- Só o motorista usa câmera custom; passageiro permanece 100% nativo. if seat == 0 or isLocalPlayerDriver() then restoreImmediatelyForVehicleExitStart("vehicle_start_exit", source) end end) addEventHandler("onClientVehicleExit", root, function(player) if player == localPlayer then snapNativeCameraInstant("vehicle_exit_confirmed", nil) kickCameraToPedAfterExit("vehicle_exit_confirmed") -- v1.0.61: depois que a saída foi confirmada, não mantemos janela longa. -- A janela longa era o que dava sensação de câmera presa no veículo. state.nativeRestoreUntil = 0 state.exitLockUntil = 0 state.enterExitPulseUntil = 0 state.pendingVehicleExit = false state.active = false state.vehicle = nil state.currentCam = nil state.currentLook = nil state.entryFrom = nil state.reverseBlend = 0 state.manualSideBlend = 0 state.manualSide = 0 state.manualRearBlend = 0 state.rearPeekUntil = 0 state.mouse3Held = false state.accelEffect = 0 state.turnAmount = 0 state.shakeAmount = 0 state.lastYaw = nil state.yawRate = 0 state.manualYaw = 0 state.manualYawTarget = 0 state.manualPitch = 0 state.manualPitchTarget = 0 state.mouseOrbitActive = false idle.active = false idle.idleStartTick = false idle.currentProfile = nil idle.fromCam = nil idle.fromLook = nil restorePlayerDefaultsNow("vehicle_exit") kickCameraToPedAfterExit("vehicle_exit") end end) -- v1.0.57: ao entrar como passageiro, garante que nada do sistema fique ativo. -- Ao entrar como motorista, o render só inicia quando o assento 0 estiver confirmado. addEventHandler("onClientVehicleEnter", root, function(player, seat) if player ~= localPlayer then return end if seat ~= 0 then -- v1.0.57: passageiro não recebe nenhum recurso do script. -- Se houver resíduo anterior, limpa uma única vez; se não houver, não faz nada. if state.active or idle.active or state.mouseCaptureActive or state.driveEmulationActive or state.currentCam or state.currentLook then disableCustomCameraForPassenger("entered_as_passenger_" .. tostring(seat)) end return end -- Motorista: não força câmera aqui. O onClientPreRender confirma o assento 0 e inicia normalmente. state.nativeRestoreUntil = 0 state.exitLockUntil = 0 state.pendingVehicleExit = false end) addEventHandler("onClientPlayerWasted", localPlayer, function() stopDynamicCamera("player_wasted") end) addEventHandler("onClientElementDestroy", root, function() if state.active and source == state.vehicle then stopDynamicCamera("vehicle_destroyed_event") end end) addEventHandler("onClientResourceStop", resourceRoot, function() scheduleRestorePlayerDefaults("resource_stop") end) addEventHandler("onClientKey", root, function(button, press) -- v1.0.41: redundância para garantir que DOWN/UP seja registrado mesmo se algum bind falhar. if Config.driveEmulation and Config.driveEmulation.keys then for _, keyList in pairs(Config.driveEmulation.keys) do if type(keyList) == "table" then for _, keyName in ipairs(keyList) do if button == keyName then setDriveKeyHeld(button, press == true) break end end end end end if not press then return end if button == "f" or button == "enter" or button == "num_enter" then local vehicle, seat = getLocalOccupiedVehicleAndSeat() if vehicle and seat ~= nil then -- v1.0.65: bloqueia saída em movimento para motorista e passageiro. if blockVehicleExitNow("enter_exit_key", vehicle) then cancelEvent() return end -- Passageiro continua nativo: parado/quase parado, deixa o MTA sair normalmente. if seat ~= 0 then return end -- Motorista parado/quase parado: restaura câmera custom e permite saída. forceNativeCameraWindow("enter_exit_key", 260) return end end -- v1.0.57: passageiro fica totalmente nativo; o script não reage a teclas de câmera/idle. if not isLocalPlayerDriver() then return end if idle.active then idleExit("key_pressed") end if state.active then idle.idleStartTick = false end end) addEventHandler("onClientCursorMove", root, function(_, _, absoluteX, absoluteY) local cfg = Config.mouseOrbit if state.ignoreNextCursorMove then state.ignoreNextCursorMove = false return end -- Se o cursor invisível foi ativado pelo MouseOrbit, usamos o delta real do mouse. if cfg and cfg.enabled and cfg.captureCursor and state.mouseCaptureOwned and state.active and not idle.active then local sx, sy = guiGetScreenSize() local cx, cy = sx / 2, sy / 2 local dx = (absoluteX or cx) - cx local dy = (absoluteY or cy) - cy if math.abs(dx) > 0.01 or math.abs(dy) > 0.01 then dx = clamp(dx, -(cfg.bigJumpIgnore or 120.0), (cfg.bigJumpIgnore or 120.0)) dy = clamp(dy, -(cfg.bigJumpIgnore or 120.0), (cfg.bigJumpIgnore or 120.0)) if math.abs(dx) > (cfg.noiseDeadzone or 0.0015) then local yawDirection = (cfg.invertHorizontal == false) and 1 or -1 state.manualYawTarget = clamp((state.manualYawTarget or 0) + dx * yawDirection * (cfg.sensitivityX or 0.105), -(cfg.maxYaw or 180.0), (cfg.maxYaw or 180.0)) state.lastMouseOrbitTick = getTickCount() end if math.abs(dy) > (cfg.noiseDeadzone or 0.0015) then state.manualPitchTarget = clamp((state.manualPitchTarget or 0) - dy * (cfg.sensitivityY or 0.045), cfg.maxPitchDown or -32.0, cfg.maxPitchUp or 44.0) state.lastMouseOrbitTick = getTickCount() end end if cfg.recenterCursor ~= false then state.ignoreNextCursorMove = true setCursorPosition(cx, cy) end idle.idleStartTick = false return end if idle.active then idleExit("cursor_moved") end idle.idleStartTick = false end) bindKey(Config.cameraView.switchKey or "v", "down", function() if not isLocalPlayerDriver() then return end if Config.cameraView and Config.cameraView.enabled then -- Se estiver na idle, sair dela suavemente antes de trocar modo de gameplay. if idle.active then idleExit("view_mode_switch") end idle.idleStartTick = false cycleCameraViewMode() end end) bindKey("mouse3", "down", function() if not isLocalPlayerDriver() then return end if Config.manualViews.enabled then if idle.active then idleExit("mouse3") end state.mouse3Held = true state.rearPeekUntil = 0 if state.active then state.manualRearBlend = math.max(state.manualRearBlend or 0, 0.12) end end end) bindKey("mouse3", "up", function() if Config.manualViews.enabled then state.mouse3Held = false if isLocalPlayerDriver() and state.active then state.rearPeekUntil = getTickCount() + (Config.manualViews.rearReleaseMs or 160) else state.rearPeekUntil = 0 end end end) addCommandHandler("dyncam", function() Config.enabled = not Config.enabled if not Config.enabled then stopDynamicCamera("command_disabled"); outputChatBox("#ff0048[DynamicCamera] #ffffffDesativada.", 255,255,255,true) else outputChatBox("#ff0048[DynamicCamera] #ffffffAtivada.", 255,255,255,true) end end) addCommandHandler("dyncamreset", function() state.currentCam = nil; state.currentLook = nil; state.reverseBlend = 0; state.manualSideBlend = 0; state.manualSide = 0; state.manualRearBlend = 0; state.rearPeekUntil = 0; state.mouse3Held = false; state.accelEffect = 0; state.turnAmount = 0; state.shakeAmount = 0; state.lastYaw = nil; state.yawRate = 0; state.manualYaw = 0; state.manualYawTarget = 0; state.manualPitch = 0; state.manualPitchTarget = 0; state.lastMouseOrbitTick = 0; state.lastPedCamRot = nil; state.lastMouseVehicleYaw = nil; state.lastManualYawApplied = 0; state.mouseOrbitActive = false; clearDriveEmulation(); clearDriveKeyHeldStates(); if releaseMouseCapture then releaseMouseCapture() end idle.active = false; idle.idleStartTick = false; idle.currentProfile = nil setFovSafe(Config.fov.default) outputChatBox("#ff0048[DynamicCamera] #ffffffSuavizações resetadas.", 255,255,255,true) end) addCommandHandler("dyncamview", function() if not isLocalPlayerDriver() then return end cycleCameraViewMode() end) addCommandHandler("dyncammousecapture", function() Config.mouseOrbit.captureCursor = not Config.mouseOrbit.captureCursor if not Config.mouseOrbit.captureCursor and releaseMouseCapture then releaseMouseCapture() clearDriveEmulation() end outputChatBox("#ff0048[DynamicCamera] #ffffffCaptura real do mouse: " .. (Config.mouseOrbit.captureCursor and "ligada" or "desligada"), 255,255,255,true) end) addCommandHandler("dyncamdriveemulation", function() Config.driveEmulation.enabled = not Config.driveEmulation.enabled if not Config.driveEmulation.enabled then clearDriveEmulation() end outputChatBox("#ff0048[DynamicCamera] #ffffffDirigibilidade por script: " .. (Config.driveEmulation.enabled and "ligada" or "desligada"), 255,255,255,true) end) addCommandHandler("dyncammouse", function() Config.mouseOrbit.enabled = not Config.mouseOrbit.enabled if not Config.mouseOrbit.enabled and releaseMouseCapture then releaseMouseCapture() end state.manualYaw = 0 state.manualYawTarget = 0 state.lastMouseOrbitTick = 0 state.lastPedCamRot = nil state.lastMouseVehicleYaw = nil state.lastManualYawApplied = 0 outputChatBox("#ff0048[DynamicCamera] #ffffffMouseOrbit: " .. (Config.mouseOrbit.enabled and "ligado" or "desligado"), 255,255,255,true) end) addCommandHandler("dyncaminfo", function() local vehicle = getDriverVehicle() local speed = vehicle and getSpeedKMH(vehicle) or 0 local mode = getCameraViewModeConfig() outputChatBox("#ff0048[DynamicCamera] #ffffffv1.0.61 | ativo: " .. tostring(state.active) .. " | idle: " .. tostring(idle.active) .. " | ligado: " .. tostring(Config.enabled), 255,255,255,true) outputChatBox("#ff0048[DynamicCamera] #ffffffvelocidade: " .. string.format("%.1f", speed) .. " km/h | turn: " .. string.format("%.2f", state.turnAmount or 0) .. " | ré: " .. string.format("%.2f", state.reverseBlend or 0), 255,255,255,true) outputChatBox("#ff0048[DynamicCamera] #ffffffmodo V: " .. tostring(mode.name or state.viewModeIndex) .. " | idle loop: " .. tostring(idle.lastProfileName), 255,255,255,true) outputChatBox("#ff0048[DynamicCamera] #ffffffperfil veículo: " .. tostring(state.currentVehicleSpecialKey or "default"), 255,255,255,true) outputChatBox("#ff0048[DynamicCamera] #ffffffmouseOrbit: " .. tostring(Config.mouseOrbit and Config.mouseOrbit.enabled) .. " | yaw manual: " .. string.format("%.1f", state.manualYaw or 0), 255,255,255,true) local _, seat = getLocalOccupiedVehicleAndSeat() outputChatBox("#ff0048[DynamicCamera] #ffffffdriveEmulation: " .. tostring(Config.driveEmulation and Config.driveEmulation.enabled) .. " | ativo: " .. tostring(state.driveEmulationActive), 255,255,255,true) outputChatBox("#ff0048[DynamicCamera] #ffffffassento: " .. tostring(seat or "fora") .. " | motorista: " .. tostring(isLocalPlayerDriver()) .. " | passageiro: " .. tostring(isLocalPlayerPassenger()), 255,255,255,true) end) if IdleConfig.enableTestCommands then addCommandHandler("testcine", function() local vehicle = getDriverVehicle() if not vehicle then outputChatBox("#ff0048[CineCam] #ffffffVocê precisa estar dirigindo.",255,255,255,true); return end state.active = state.active or false if not state.active then startDynamicCamera(vehicle) end idleStart() outputChatBox("#ff0048[CineCam] #ffffffIdle cinematográfica integrada ativada.",255,255,255,true) end) addCommandHandler("stopcine", function() if idle.active then idleExit("manual_command"); outputChatBox("#ff0048[CineCam] #ffffffIdle desligada, chase assumindo suave.",255,255,255,true) else outputChatBox("#ff0048[CineCam] #ffffffIdle já está desligada.",255,255,255,true) end end) end