From 023a564b8cb01f6d49eba3e8c87a0244b33026ec Mon Sep 17 00:00:00 2001 From: carvibox <236358783+carvibox@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:06:37 +0100 Subject: [PATCH] feat: hand camera settings over to SteadyCam when it is loaded When SteadyCam is loaded, Combat Mode stops writing turn speed, shoulder offset, dynamic pitch and the motion-sickness gates, the same way it already does for DynamicCam. SteadyCam takes precedence over DynamicCam (it keeps running and offers the player to disable DynamicCam). - Runtime: CM.SteadyCam / CM.CameraOwner ("SteadyCam" or "DynamicCam"); SteadyCam-specific chat notice. - CVarManager: DynamicCam checks now use CM.CameraOwner; never writes the shoulder offset with SteadyCam; Motion Sickness Protection defers to SteadyCamAPI.RespectsMotionSickness() (Target Focus stays in Combat Mode). - Options: delegated camera settings show "Delegated to SteadyCam", localized to the client language. Co-Authored-By: Claude Opus 5.5 --- CombatMode/Core/Runtime/CVarManager.lua | 31 ++++++---- CombatMode/Core/Runtime/Runtime.lua | 12 +++- CombatMode/UI/Options/Tabs/TabGeneral.lua | 59 ++++++++++++++++--- .../UI/Options/Tabs/TabReticleTargeting.lua | 5 +- 4 files changed, 87 insertions(+), 20 deletions(-) diff --git a/CombatMode/Core/Runtime/CVarManager.lua b/CombatMode/Core/Runtime/CVarManager.lua index 9ebf123..a95a98b 100644 --- a/CombatMode/Core/Runtime/CVarManager.lua +++ b/CombatMode/Core/Runtime/CVarManager.lua @@ -360,9 +360,16 @@ end --- Motion Sickness Protection: do not drive ActionCam feature CVars / leave MS alone. local function RespectMotionSickness() + -- Delegated: SteadyCam's own Motion Sickness Protection decides (it also gates + -- Focus Locked Target here). + if CM.SteadyCam then + local api = _G.SteadyCamAPI + return api ~= nil and api.RespectsMotionSickness ~= nil and api.RespectsMotionSickness() == true + end local g = CM.DB and CM.DB.global return g and g.respectMotionSickness == true end +CM.MotionSicknessProtectionOn = RespectMotionSickness local function ShoulderFadeDuration() return (CM.Constants and CM.Constants.MouseLookCameraFadeDuration) or 0.35 @@ -396,6 +403,10 @@ end --- Desired shoulder, or nil to stop writing (DynamicCam owns the CVar). local function DesiredShoulderOffset() + -- SteadyCam frames the character on foot and on every mount: never write. + if CM.SteadyCam then + return nil + end -- Protection clears CM-owned shoulder; with DynamicCam, stop writing entirely. if RespectMotionSickness() then if CM.DynamicCam then @@ -563,7 +574,7 @@ function CM.SetShoulderOffset() local desired = DesiredShoulderOffset() EnsureShoulderAtDesired(desired) if desired == nil then - CM.DebugPrint("Shoulder Offset relinquished to DynamicCam") + CM.DebugPrint("Shoulder Offset relinquished to " .. tostring(CM.CameraOwner)) else CM.DebugPrint("Shoulder Offset target " .. tostring(desired)) end @@ -603,7 +614,7 @@ end --- Do not key this off freelook alone: flipping MS with Mouse Look lock/unlock snaps --- skyriding FOV/zoom when Dynamic Pitch is off (pitch-on keeps MS sticky at 0). local function ShoulderNeedsActionCam() - if CM.DynamicCam then + if CM.CameraOwner then return false end if ShoulderFollowsMouseLook() then @@ -654,7 +665,7 @@ end --- Restore MS CVars from prior snapshot (or Blizzard-on defaults). Used when enabling --- respectMotionSickness or when ActionCam no longer needs MS off. function CM.RestoreMotionSicknessFromSnapshot() - if CM.DynamicCam then + if CM.CameraOwner then return end local keep, reduce = MotionSicknessReleaseValues() @@ -662,7 +673,7 @@ function CM.RestoreMotionSicknessFromSnapshot() end function CM.ApplyActionCamMotionSicknessGate() - if CM.DynamicCam then + if CM.CameraOwner then return end if RespectMotionSickness() then @@ -686,7 +697,7 @@ end --- Sticky Dynamic Pitch (option-gated, not freelook). Pads + master CVar + MS gate --- so unlock / option toggles actually take effect. Forced off under Motion Sickness Protection. function CM.SetDynamicPitch() - if CM.DynamicCam then + if CM.CameraOwner then return end local wantPitch = CM.DB @@ -703,7 +714,7 @@ end --- Apply Mouse Look camera chrome while freelook is locked (MS off when we own it, shoulder). function CM.ApplyMouseLookCamera() - if not CM.DynamicCam then + if not CM.CameraOwner then CM.ApplyActionCamMotionSicknessGate() end CM.SetShoulderOffset() @@ -713,7 +724,7 @@ end --- Clear Mouse Look camera chrome while freelook is unlocked (tween shoulder to 0; MS gate). function CM.ClearMouseLookCamera() CM.SetShoulderOffset() - if not CM.DynamicCam then + if not CM.CameraOwner then CM.ApplyActionCamMotionSicknessGate() end CM.DebugPrint("Mouse Look camera cleared") @@ -742,7 +753,7 @@ function CM.SyncTargetFocusFromFocusUnit() CM.SetCVar("test_cameraTargetFocusEnemyEnable", want and 1 or 0) if want then WriteMotionSicknessCVars(0, 0) - elseif not CM.DynamicCam then + elseif not CM.CameraOwner then CM.ApplyActionCamMotionSicknessGate() end end @@ -758,7 +769,7 @@ function CM.ApplyMotionSicknessProtectionState() if CM.SyncTargetFocusFromFocusUnit then CM.SyncTargetFocusFromFocusUnit() end - if not CM.DynamicCam then + if not CM.CameraOwner then CM.ApplyActionCamMotionSicknessGate() end end @@ -778,7 +789,7 @@ function CM.ConfigStickyCrosshair(CVarType) end function CM.SetMouseLookSpeed() - if CM.DynamicCam then + if CM.CameraOwner then return end diff --git a/CombatMode/Core/Runtime/Runtime.lua b/CombatMode/Core/Runtime/Runtime.lua index 9d6afeb..46ac473 100644 --- a/CombatMode/Core/Runtime/Runtime.lua +++ b/CombatMode/Core/Runtime/Runtime.lua @@ -185,7 +185,17 @@ end local function IsDCLoaded() local loaded = IsAddOnLoaded("DynamicCam") CM.DynamicCam = loaded and true or false - if CM.DynamicCam and not CM.DB.global.silenceAlerts then + -- SteadyCam owns turn speed, shoulder offset, dynamic pitch and the motion-sickness + -- gates (Target Focus stays here). It takes precedence over DynamicCam: SteadyCam + -- keeps running and offers the player to disable DynamicCam itself. + CM.SteadyCam = IsAddOnLoaded("SteadyCam") and true or false + CM.CameraOwner = (CM.SteadyCam and "SteadyCam") or (CM.DynamicCam and "DynamicCam") or nil + if CM.SteadyCam and not CM.DB.global.silenceAlerts then + print( + CM.Constants.BasePrintMsg + .. "|cff909090: |cffE52B50SteadyCam detected!|r Handing over turn speed, shoulder, and dynamic pitch.|r" + ) + elseif CM.DynamicCam and not CM.DB.global.silenceAlerts then print( CM.Constants.BasePrintMsg .. "|cff909090: |cffE52B50DynamicCam detected!|r Handing over turn speed, shoulder, and dynamic pitch.|r" diff --git a/CombatMode/UI/Options/Tabs/TabGeneral.lua b/CombatMode/UI/Options/Tabs/TabGeneral.lua index bcd2edc..a239634 100644 --- a/CombatMode/UI/Options/Tabs/TabGeneral.lua +++ b/CombatMode/UI/Options/Tabs/TabGeneral.lua @@ -49,14 +49,43 @@ local INTERACT_UNIT_VALUES = { } local INTERACT_UNIT_ORDER = { "mouseover", "target" } +-- Shown over camera controls handed to SteadyCam, in the game's text language. +local DELEGATED_TO_STEADYCAM = { + enUS = "Delegated to SteadyCam", + esES = "Delegado a SteadyCam", + esMX = "Delegado a SteadyCam", + deDE = "An SteadyCam übergeben", + frFR = "Délégué à SteadyCam", + itIT = "Delegato a SteadyCam", + ptBR = "Delegado ao SteadyCam", + ruRU = "Передано SteadyCam", + koKR = "SteadyCam에 위임됨", + zhCN = "已交由 SteadyCam 控制", + zhTW = "已交由 SteadyCam 控制", +} + +--- Watermark for controls another camera addon owns (nil when Combat Mode owns them). +local function CameraOwnerWatermark() + if CM.SteadyCam then + return DELEGATED_TO_STEADYCAM[_G.GetLocale()] or DELEGATED_TO_STEADYCAM.enUS + end + if CM.CameraOwner then + return "Control relinquished to " .. CM.CameraOwner + end + return nil +end + local function RespectMotionSicknessOn() + if CM.MotionSicknessProtectionOn then + return CM.MotionSicknessProtectionOn() == true -- SteadyCam's when delegated + end return CM.DB.global.respectMotionSickness == true end --- Watermark when camera ActionCam controls are locked by DynamicCam or Respect MS. local function CameraFeatureWatermark() - if CM.DynamicCam then - return "Control relinquished to DynamicCam" + if CM.CameraOwner then + return CameraOwnerWatermark() end if RespectMotionSicknessOn() then return "Disabled while Motion Sickness Protection is on" @@ -65,7 +94,7 @@ local function CameraFeatureWatermark() end local function CameraFeatureDisabled() - return CM.DynamicCam or RespectMotionSicknessOn() + return CM.CameraOwner ~= nil or RespectMotionSicknessOn() end --- Primary + alternate interact binding commands from CM.DB.global.interactUnit. @@ -155,7 +184,7 @@ UI.Options.AddTab({ max = 180, step = 10, default = 100, - watermarkWhenDisabled = "Control relinquished to DynamicCam", + watermarkWhenDisabled = CameraOwnerWatermark, get = function() return CM.DB.global.mouseLookSpeed end, @@ -164,7 +193,7 @@ UI.Options.AddTab({ CM.SetMouseLookSpeed() end, disabled = function() - return CM.DynamicCam + return CM.CameraOwner ~= nil end, }) ctx:Toggle({ @@ -221,6 +250,16 @@ UI.Options.AddTab({ ctx:Toggle({ label = "Motion Sickness Protection", desc = "Prevents Combat Mode from overriding Blizzard's Motion Sickness settings.", + -- Delegated with the rest of the camera: SteadyCam has the same option. + watermarkWhenDisabled = function() + if CM.SteadyCam then + return CameraOwnerWatermark() + end + return nil + end, + disabled = function() + return CM.SteadyCam + end, get = function() return RespectMotionSicknessOn() end, @@ -293,8 +332,12 @@ UI.Options.AddTab({ label = "Disable Offset With Mouselook", desc = "Shoulder Offset eases to 0 when disabling Mouse Look." .. "\nWhen off, Shoulder Offset stays constant at all times.", - -- Still editable with DynamicCam (unlock→0 / restore path); only MS Protection locks it. + -- Still editable with DynamicCam (unlock→0 / restore path); SteadyCam and MS + -- Protection lock it. watermarkWhenDisabled = function() + if CM.SteadyCam then + return CameraOwnerWatermark() + end if RespectMotionSicknessOn() then return "Disabled while Motion Sickness Protection is on" end @@ -309,7 +352,9 @@ UI.Options.AddTab({ CM.SetShoulderOffset() end end, - disabled = RespectMotionSicknessOn, + disabled = function() + return CM.SteadyCam or RespectMotionSicknessOn() + end, }) ctx:Gap() diff --git a/CombatMode/UI/Options/Tabs/TabReticleTargeting.lua b/CombatMode/UI/Options/Tabs/TabReticleTargeting.lua index e38adf8..2e2f013 100644 --- a/CombatMode/UI/Options/Tabs/TabReticleTargeting.lua +++ b/CombatMode/UI/Options/Tabs/TabReticleTargeting.lua @@ -218,7 +218,7 @@ UI.Options.AddTab({ label = "Focus Locked Target", desc = "Pulls the camera toward your locked target, keeping it in focus.", watermarkWhenDisabled = function() - if CM.DB.global.respectMotionSickness == true then + if CM.MotionSicknessProtectionOn and CM.MotionSicknessProtectionOn() == true then return "Disabled while Motion Sickness Protection is on" end return nil @@ -233,7 +233,8 @@ UI.Options.AddTab({ end end, disabled = function() - return not CM.DB.char.reticleTargeting or CM.DB.global.respectMotionSickness == true + return not CM.DB.char.reticleTargeting + or (CM.MotionSicknessProtectionOn and CM.MotionSicknessProtectionOn() == true) end, }) end,