diff --git a/crates/benilla-app/assets/ui/MapCanvas.lua b/crates/benilla-app/assets/ui/MapCanvas.lua index 4af321d0..5d6c3794 100644 --- a/crates/benilla-app/assets/ui/MapCanvas.lua +++ b/crates/benilla-app/assets/ui/MapCanvas.lua @@ -61,10 +61,36 @@ local SCALE_SNAP = 0.001 -- notch. That was tolerable only while a notch could not accumulate, because the target was never -- more than one rung away; once notches add up the glide is the thing you feel. grom, 2026-11-04: -- *"scroll is better but feels pretty slippery, like scrolls past where I stop scrolling."* At 24 --- the half-life is 0.29 s and a multi-rung spin lands about where the wheel stopped. The pan's is --- left alone: dragging writes the scroll directly and never eases. -local PAN_RATE = 10 -local SCALE_RATE = 24 +-- the half-life is 0.29 s and a multi-rung spin lands about where the wheel stopped. +-- +-- 0.29 s was still the wrong answer. grom, 2026-11-05: *"its the maps motion that is laggy."* +-- That is the same constant read from the other side — at 60 fps a 0.29 s half-life closes 90% of +-- a notch in 58 frames, so the map keeps drifting for a second after the wheel stops, and the two +-- reports are "it overshoots" and "it lags behind" about one glide. Guessing a third number would +-- be the third guess, so it is a SETTING now, and these two are the ends of its range rather than +-- the behaviour: `mapMotionSmoothing` is a percentage of [`SMOOTH_MAX_HALFLIFE`], and 0 is no ease +-- at all. +-- +-- The top of the slider is the 0.29 s half-life he called laggy, because that is the slowest +-- anyone could want and there is no reason to offer slower. The default is a quarter of it: +-- 0.075 s, which closes 90% of a wheel notch in 0.25 s and the last thousandth in 0.69 s. +-- Measured over a full rung (scale 1 -> 1.6) at 60 fps, frames to settle within 0.001: +-- 0% = 1, 10% = 17, 20% = 34, 50% = 84, 100% = 167 (the old constant's own 167). +local SMOOTH_MAX_HALFLIFE = 0.30 +-- The default percentage, and the fallback when the CVar is missing (a bare VM, a test). +local SMOOTH_DEFAULT = 25 +-- `nil` means no ease: snap to the target this frame. `RATE` is the file's own relation, +-- `7 / halflife`, so the arithmetic below is unchanged. +local function SmoothRate() + local pct = tonumber(GetCVar and GetCVar("mapMotionSmoothing") or nil) or SMOOTH_DEFAULT + if pct <= 0 then + return nil + end + if pct > 100 then + pct = 100 + end + return 7 / (pct / 100 * SMOOTH_MAX_HALFLIFE) +end -- A drag under this many pixels is still a click, which is what `WouldCursorPositionBeClick` -- answers; Era's own threshold. local CLICK_SLOP = 5 @@ -773,7 +799,7 @@ function MapCanvasScrollControllerMixin:SetZoomTarget(scale, x, y) end -- Which rung a wheel notch steps FROM: where the view is heading, not where the ease has got --- to. `currentScale` closes about 63% of the gap a second (`SCALE_RATE`), so a second notch +-- to. `currentScale` eases toward the target (`SmoothRate`), so a second notch -- arriving a frame after the first re-reads a scale that has barely moved, finds the same rung -- under it, and re-sets the target it already has. Measured at the player's own window size -- with his addons loaded: four wheel-down notches over the map moved the ladder ONE step @@ -1173,7 +1199,10 @@ function MapCanvasScrollControllerMixin:OnUpdate(elapsed) end end else - local rate = 1 - math.pow(0.5, (elapsed or 0) * PAN_RATE / 7) + -- One rate for both eases, read once a frame: the two are one gesture to a player, and a + -- zoom that settles while the pan is still drifting reads as the pan being broken. + local smooth = SmoothRate() + local rate = smooth and (1 - math.pow(0.5, (elapsed or 0) * smooth / 7)) or 1 local tx, ty = self.targetScrollX or 0.5, self.targetScrollY or 0.5 local cx, cy = self.currentScrollX or 0.5, self.currentScrollY or 0.5 if math.abs(tx - cx) > PAN_SNAP or math.abs(ty - cy) > PAN_SNAP then @@ -1189,7 +1218,8 @@ function MapCanvasScrollControllerMixin:OnUpdate(elapsed) local scaled = false local ts, cs = self.targetScale or 1, self.currentScale or 1 if math.abs(ts - cs) > SCALE_SNAP then - local rate = 1 - math.pow(0.5, (elapsed or 0) * SCALE_RATE / 7) + local smooth = SmoothRate() + local rate = smooth and (1 - math.pow(0.5, (elapsed or 0) * smooth / 7)) or 1 self.currentScale = cs + (ts - cs) * rate scaled = true elseif cs ~= ts then diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 31dbb614..9d6702d0 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -479,6 +479,13 @@ .. "In Windowed (Fullscreen) the window already fills the screen, so this sets the " .. "size it returns to in Windowed. Takes effect on Apply." + -- 1.12's map neither pans nor zooms, so it has no such setting and no reference value. + BENILLA_TOOLTIP_MAP_MOTION_SMOOTHING = + "How long the world map keeps gliding after you stop scrolling or dragging.\n\n" + .. "At 0 the map goes exactly where you put it, the same frame. Higher is smoother " + .. "and slower to settle; past about half way the map is still moving a second after " + .. "the wheel stopped." + -- 1.12 has no such setting: its map does not fade, because its map cannot be read while -- you walk either way (it covers the screen and takes the keyboard). BENILLA_TOOLTIP_MAP_FADE_WHILE_MOVING = @@ -1582,7 +1589,7 @@ "RowProfanityFilter", "RowShowTutorials", "RowTipOfTheDay", "RowNewbieTips", -- Appended, as this list's own rule says: the tests assert positional indices. - "RowMapFadeWhileMoving", + "RowMapFadeWhileMoving", "RowMapMotionSmoothing", }, -- 1.12's chat box (UIOptionsFrame.xml:751-840). Not shipped: Simple Chat, which needs -- FCF_Set_SimpleChat's re-layout, Lock Chat Settings and Guild Recruitment. @@ -5064,11 +5071,23 @@ - + + + OptionsRow_OnLoad(self, "mapMotionSmoothing", + "World Map Motion Smoothing", + "BENILLA_TOOLTIP_MAP_MOTION_SMOOTHING") + OptionsSliderRow_Setup(self, 0, 100, 10, "pct") + + + + + + + OptionsRow_OnLoad(self, "mapFadeWhileMoving", "Fade World Map While Moving", diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index c49ef7af..44c75e25 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -1952,6 +1952,15 @@ pub(crate) const REGISTERED: &[Registered] = &[ // frame verb, and `IsPlayerMoving()` is already a verb — so the whole feature is Lua over a // CVar, and the CVar is what gives it a home in the Options window and in `config.toml`. // 0 switches it off; the Options row offers 0 and 10..90. + // Same shape and same reader as `mapFadeWhileMoving` below. + ours( + "mapMotionSmoothing", + "25", + "how long the world map's pan and zoom keep gliding after the input stops, as a percent \ + of a 0.3 s half-life; 0 snaps. 1.12's map neither pans nor zooms, so there is no \ + reference value — grom has now reported both ends of one guess (2026-11-04 \ + \"slippery\", 2026-11-05 \"laggy\"), which is what makes it a setting", + ), ours( "mapFadeWhileMoving", "80", @@ -5832,6 +5841,10 @@ mod tests { "mapFadeWhileMoving", "MapCanvas.lua's `FadeWhileMoving`, every frame the map is open", ), + ( + "mapMotionSmoothing", + "MapCanvas.lua's `SmoothRate`, read by the pan and zoom eases", + ), // The five per-kind plate rows with no gate of their own. They exist because an addon // written for a later client greys a whole row on a nil `GetCVar` — registering is the // answer to "does this client have this setting", and each default below is this diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs index 1be2c38a..991845ef 100644 --- a/crates/benilla-app/src/ui_script/options_tests.rs +++ b/crates/benilla-app/src/ui_script/options_tests.rs @@ -3766,6 +3766,11 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() { "BENILLA_TOOLTIP_MAP_FADE_WHILE_MOVING", "InterfaceRowMapFadeWhileMoving", ), + // The world map's glide, a setting because two reports bracketed one guess. + ( + "BENILLA_TOOLTIP_MAP_MOTION_SMOOTHING", + "InterfaceRowMapMotionSmoothing", + ), // MONKEY (advanced graphics): the whole Advanced Graphics page. Every row on it is // benilla's own mechanism — 1.12 has no realtime shadow, no fixture-lit interior and // no invented light — so there is no `OPTION_TOOLTIP_*` any of them could resolve, @@ -3902,8 +3907,8 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() { // GFX (sharpening) / (field of view): one row each, on the two different pages. 136 -> 138. // GFX (froxel volumetrics): the shadowed-air dial beside the haze tier. 138 -> 139. // GFX (foliage distance): the ground-clutter horizon. 139 -> 140. - // The world map's fade-while-moving dial. 140 -> 141. - assert_eq!(checked, 141, "every tipped row carries a live key"); + // The world map's fade-while-moving dial. 140 -> 141; its motion-smoothing dial, 141 -> 142. + assert_eq!(checked, 142, "every tipped row carries a live key"); assert_eq!( untipped, vec![ @@ -4012,7 +4017,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() { // GFX (shadow rig): the five Advanced Graphics shadow rows. 119 -> 124. // GFX (interior rig): the ten interior-lighting rows. 124 -> 134. // GFX (auto-exposure): the tier and its blend. 134 -> 136. - assert_eq!(raised, 141, "every row but Auto Loot raises a description"); + assert_eq!(raised, 142, "every row but Auto Loot raises a description"); } /// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a