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