Skip to content

Commit 6914d23

Browse files
committed
release: prepare 2.4.4
- restore TV Board Zoom after reload hydration gaps - stabilize zoom transforms across responsive layout changes
1 parent 14209f6 commit 6914d23

11 files changed

Lines changed: 295 additions & 24 deletions

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ zum nächsten Release-Commit vorübergehend auf `HEAD` zeigen.
1212
Dieses Repository führt keine `Unreleased`-Sektion. Jeder dokumentierte Eintrag gehört
1313
direkt zu einer versionierten Release-Sektion.
1414

15+
## [2.4.4] - 2026-06-19
16+
17+
### Fixed
18+
19+
- Nutzerwirkung: `TV Board Zoom` stellt einen sichtbaren direkten Checkout nach einem Seiten-Reload wieder her, auch wenn der interne Match-Turn noch nicht vollständig geladen ist.
20+
Technik: Ein eng begrenzter Hydration-Fallback akzeptiert ausschließlich eine sichtbare, gegen den aktiven Score validierte Ein-Dart-Finish-Suggestion und übergibt danach wieder an den regulären Game-State-Pfad.
21+
- Nutzerwirkung: Der Board-Zoom flackert bei responsiven Größenänderungen nicht mehr, besonders bei Checkout-Zielen im unteren Board-Bereich.
22+
Technik: Die Transform-Normalisierung priorisiert aktuelle untransformierte Layoutmaße, konvergiert nach Board-Resizes auf einen stabilen Transform und ist gegen wiederholte Reapply-Aufrufe abgesichert.
23+
1524
## [2.4.3] - 2026-06-19
1625

1726
### Added
@@ -1731,7 +1740,8 @@ direkt zu einer versionierten Release-Sektion.
17311740
und Regressionstests eingeführt und die generierten README-/FEATURES-Texte wurden
17321741
entsprechend synchronisiert.
17331742

1734-
[2.4.3]: https://github.com/thomasasen/autodarts-xconfig/compare/73900ce...HEAD
1743+
[2.4.4]: https://github.com/thomasasen/autodarts-xconfig/compare/e93e11d...HEAD
1744+
[2.4.3]: https://github.com/thomasasen/autodarts-xconfig/compare/73900ce...e93e11d
17351745
[2.4.2]: https://github.com/thomasasen/autodarts-xconfig/compare/24f6686...73900ce
17361746
[2.4.1]: https://github.com/thomasasen/autodarts-xconfig/compare/3803021...24f6686
17371747
[2.4.0]: https://github.com/thomasasen/autodarts-xconfig/compare/55a936b...3803021

dist/autodarts-xconfig.meta.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name autodarts-xconfig
33
// @namespace https://github.com/thomasasen/autodarts-xconfig
4-
// @version 2.4.3
4+
// @version 2.4.4
55
// @description Modular, side-effect resistant Tampermonkey runtime for Autodarts enhancements.
66
// @author Thomas Asen
77
// @license MIT

dist/autodarts-xconfig.user.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name autodarts-xconfig
33
// @namespace https://github.com/thomasasen/autodarts-xconfig
4-
// @version 2.4.3
4+
// @version 2.4.4
55
// @description Modular, side-effect resistant Tampermonkey runtime for Autodarts enhancements.
66
// @author Thomas Asen
77
// @license MIT
@@ -10389,8 +10389,10 @@ ${scopedClassSelector(selectorPrefix, EFFECT_CLASSES["fade-blink"])} {
1038910389
const normalizedRect = normalizeRect(rect);
1039010390
const scale = Number(zoomTransform?.scale);
1039110391
const layoutSize = getNodeLayoutSize(expectedNode);
10392-
const baseWidth = Number.isFinite(zoomTransform?.baseWidth) ? Number(zoomTransform.baseWidth) : layoutSize.width;
10393-
const baseHeight = Number.isFinite(zoomTransform?.baseHeight) ? Number(zoomTransform.baseHeight) : layoutSize.height;
10392+
const storedBaseWidth = Number(zoomTransform?.baseWidth);
10393+
const storedBaseHeight = Number(zoomTransform?.baseHeight);
10394+
const baseWidth = layoutSize.width > 0 ? layoutSize.width : storedBaseWidth;
10395+
const baseHeight = layoutSize.height > 0 ? layoutSize.height : storedBaseHeight;
1039410396
if (!normalizedRect || !(Number.isFinite(scale) && scale > 1) || !(Number.isFinite(baseWidth) && baseWidth > 0) || !(Number.isFinite(baseHeight) && baseHeight > 0)) {
1039510397
return false;
1039610398
}
@@ -10681,6 +10683,32 @@ ${scopedClassSelector(selectorPrefix, EFFECT_CLASSES["fade-blink"])} {
1068110683
turnChanged: turnId !== state.lastTurnId
1068210684
};
1068310685
}
10686+
function resolveHydrationCheckoutIntent({
10687+
gameState,
10688+
x01Rules,
10689+
state,
10690+
documentRef,
10691+
windowRef,
10692+
config
10693+
}) {
10694+
if (!config?.checkoutZoomEnabled) {
10695+
return null;
10696+
}
10697+
const checkoutContext = resolveX01CheckoutContext({
10698+
gameState,
10699+
documentRef,
10700+
windowRef,
10701+
dartsRemaining: 1,
10702+
x01Rules
10703+
});
10704+
const checkoutSurface = checkoutContext.checkoutSurface;
10705+
const visibleSegments = checkoutSurface.visibleRouteSegments;
10706+
const finishSegment = checkoutSurface.authoritativeFinishSegment;
10707+
if (checkoutSurface.selectionSource !== "validated-visible-route" || visibleSegments.length !== 1 || visibleSegments[0] !== finishSegment || !checkoutSurface.canUseAuthoritativeFinishNow) {
10708+
return null;
10709+
}
10710+
return buildAndStoreIntent(state, "checkout", finishSegment);
10711+
}
1068410712
function resetZoomIntentForTurnChange(state) {
1068510713
state.holdUntilTs = 0;
1068610714
if (!state.stickyUntilLegEnd) {
@@ -10936,7 +10964,14 @@ ${scopedClassSelector(selectorPrefix, EFFECT_CLASSES["fade-blink"])} {
1093610964
syncBoundaryTokenState(state, resolveGameBoundaryToken(gameState));
1093710965
const turnProgress = resolveTurnProgressState(state, gameState);
1093810966
if (!turnProgress) {
10939-
return null;
10967+
return resolveHydrationCheckoutIntent({
10968+
gameState,
10969+
x01Rules,
10970+
state,
10971+
documentRef,
10972+
windowRef,
10973+
config
10974+
});
1094010975
}
1094110976
const { throws, turnId, throwCount, previousThrowCount } = turnProgress;
1094210977
let turnChanged = turnProgress.turnChanged;
@@ -41320,7 +41355,7 @@ ${buildTurnSuggestionDartGuardStyleBlock()}`;
4132041355

4132141356
// src/core/bootstrap.js
4132241357
var GLOBAL_NAMESPACE_KEY = "__adXConfig";
41323-
var API_VERSION = "2.4.3";
41358+
var API_VERSION = "2.4.4";
4132441359
var STARTUP_DEFER_INTERVAL_MS = 16;
4132541360
function getWindowTimerApi(windowRef) {
4132641361
let setTimeoutRef = null;

loader/autodarts-xconfig.user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name autodarts-xconfig
33
// @namespace https://github.com/thomasasen/autodarts-xconfig
4-
// @version 2.4.3
4+
// @version 2.4.4
55
// @description Modular, side-effect resistant Tampermonkey runtime for Autodarts enhancements.
66
// @author Thomas Asen
77
// @license MIT

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "autodarts-xconfig",
3-
"version": "2.4.3",
3+
"version": "2.4.4",
44
"description": "Clean successor architecture for autodarts xConfig userscript modules",
55
"type": "module",
66
"imports": {

src/core/bootstrap.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { createListenerRegistry } from "./listener-registry.js";
1313
import { createObserverRegistry } from "./observer-registry.js";
1414

1515
const GLOBAL_NAMESPACE_KEY = "__adXConfig";
16-
const API_VERSION = "2.4.3";
16+
const API_VERSION = "2.4.4";
1717
const STARTUP_DEFER_INTERVAL_MS = 16;
1818

1919
function getWindowTimerApi(windowRef) {

src/features/tv-board-zoom/logic.js

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -862,12 +862,10 @@ function shouldNormalizeRectForActiveZoom(rect, zoomTransform, expectedNode = nu
862862
const normalizedRect = normalizeRect(rect);
863863
const scale = Number(zoomTransform?.scale);
864864
const layoutSize = getNodeLayoutSize(expectedNode);
865-
const baseWidth = Number.isFinite(zoomTransform?.baseWidth)
866-
? Number(zoomTransform.baseWidth)
867-
: layoutSize.width;
868-
const baseHeight = Number.isFinite(zoomTransform?.baseHeight)
869-
? Number(zoomTransform.baseHeight)
870-
: layoutSize.height;
865+
const storedBaseWidth = Number(zoomTransform?.baseWidth);
866+
const storedBaseHeight = Number(zoomTransform?.baseHeight);
867+
const baseWidth = layoutSize.width > 0 ? layoutSize.width : storedBaseWidth;
868+
const baseHeight = layoutSize.height > 0 ? layoutSize.height : storedBaseHeight;
871869
if (
872870
!normalizedRect ||
873871
!(Number.isFinite(scale) && scale > 1) ||
@@ -1224,6 +1222,41 @@ function resolveTurnProgressState(state, gameState) {
12241222
};
12251223
}
12261224

1225+
function resolveHydrationCheckoutIntent({
1226+
gameState,
1227+
x01Rules,
1228+
state,
1229+
documentRef,
1230+
windowRef,
1231+
config,
1232+
}) {
1233+
if (!config?.checkoutZoomEnabled) {
1234+
return null;
1235+
}
1236+
1237+
const checkoutContext = resolveX01CheckoutContext({
1238+
gameState,
1239+
documentRef,
1240+
windowRef,
1241+
dartsRemaining: 1,
1242+
x01Rules,
1243+
});
1244+
const checkoutSurface = checkoutContext.checkoutSurface;
1245+
const visibleSegments = checkoutSurface.visibleRouteSegments;
1246+
const finishSegment = checkoutSurface.authoritativeFinishSegment;
1247+
1248+
if (
1249+
checkoutSurface.selectionSource !== "validated-visible-route" ||
1250+
visibleSegments.length !== 1 ||
1251+
visibleSegments[0] !== finishSegment ||
1252+
!checkoutSurface.canUseAuthoritativeFinishNow
1253+
) {
1254+
return null;
1255+
}
1256+
1257+
return buildAndStoreIntent(state, "checkout", finishSegment);
1258+
}
1259+
12271260
function resetZoomIntentForTurnChange(state) {
12281261
state.holdUntilTs = 0;
12291262
if (!state.stickyUntilLegEnd) {
@@ -1542,7 +1575,14 @@ export function computeZoomIntent(options = {}) {
15421575

15431576
const turnProgress = resolveTurnProgressState(state, gameState);
15441577
if (!turnProgress) {
1545-
return null;
1578+
return resolveHydrationCheckoutIntent({
1579+
gameState,
1580+
x01Rules,
1581+
state,
1582+
documentRef,
1583+
windowRef,
1584+
config,
1585+
});
15461586
}
15471587

15481588
const { throws, turnId, throwCount, previousThrowCount } = turnProgress;

tests/runtime/tv-board-zoom-layout.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,68 @@ test("tv-board-zoom keeps the active translated zoom when layout reads reflect t
927927
assert.equal(String(targetNode.style.transform || ""), firstTransform);
928928
});
929929

930+
test("tv-board-zoom converges after an active lower-board zoom changes layout size", () => {
931+
const { documentRef, windowRef, hostNode, targetNode, boardSvg } = createZoomFixture();
932+
const state = createZoomState();
933+
const speedConfig = {
934+
zoomInMs: 180,
935+
zoomOutMs: 220,
936+
easingIn: "ease-in",
937+
easingOut: "ease-out",
938+
};
939+
const intent = { reason: "checkout", segment: "D17" };
940+
let baseRect = { left: 918, top: 225.3, width: 440, height: 440 };
941+
const readTransformedRect = () =>
942+
applyCurrentStyleTransformToRect(targetNode, {
943+
...baseRect,
944+
right: baseRect.left + baseRect.width,
945+
bottom: baseRect.top + baseRect.height,
946+
});
947+
948+
targetNode.offsetWidth = baseRect.width;
949+
targetNode.offsetHeight = baseRect.height;
950+
targetNode.clientWidth = baseRect.width;
951+
targetNode.clientHeight = baseRect.height;
952+
boardSvg.clientWidth = baseRect.width;
953+
boardSvg.clientHeight = baseRect.height;
954+
targetNode.getBoundingClientRect = readTransformedRect;
955+
boardSvg.getBoundingClientRect = readTransformedRect;
956+
957+
applyZoom(
958+
{ targetNode, hostNode, boardSvg },
959+
2.75,
960+
speedConfig,
961+
intent,
962+
state,
963+
{ x01Rules, windowRef, documentRef }
964+
);
965+
const initialTransform = String(targetNode.style.transform || "");
966+
967+
baseRect = { ...baseRect, width: 456, height: 456 };
968+
targetNode.offsetWidth = baseRect.width;
969+
targetNode.offsetHeight = baseRect.height;
970+
targetNode.clientWidth = baseRect.width;
971+
targetNode.clientHeight = baseRect.height;
972+
boardSvg.clientWidth = baseRect.width;
973+
boardSvg.clientHeight = baseRect.height;
974+
975+
const transformsAfterResize = Array.from({ length: 3 }, () => {
976+
applyZoom(
977+
{ targetNode, hostNode, boardSvg },
978+
2.75,
979+
speedConfig,
980+
intent,
981+
state,
982+
{ x01Rules, windowRef, documentRef }
983+
);
984+
return String(targetNode.style.transform || "");
985+
});
986+
987+
assert.notEqual(transformsAfterResize[0], initialTransform);
988+
assert.equal(new Set(transformsAfterResize).size, 1);
989+
assert.equal(targetNode.style.transition, "none");
990+
});
991+
930992
test("tv-board-zoom converges when a second zoom state reads an already-transformed board", () => {
931993
const { documentRef, windowRef, hostNode, targetNode, boardSvg } = createZoomFixture();
932994
const firstState = createZoomState();

tests/runtime/tv-board-zoom-runtime.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,54 @@ test("tv-board-zoom applies a direct finish zoom at runtime for D5 in finish-onl
227227
}
228228
});
229229

230+
test("tv-board-zoom applies a visible checkout after reload before game-state hydration", async () => {
231+
const documentRef = new FakeDocument();
232+
documentRef.activeScoreElement.textContent = "4";
233+
documentRef.suggestionElement.textContent = "D2";
234+
documentRef.suggestionElement.__rect = { left: 320, top: 16, width: 180, height: 48 };
235+
const windowRef = createFakeWindow({ documentRef });
236+
const timers = createFakeTimerHarness();
237+
timers.installOnWindow(windowRef);
238+
timers.installGlobals();
239+
const { hostNode, targetNode } = installZoomFixture(documentRef);
240+
const gameState = {
241+
isX01Variant() {
242+
return true;
243+
},
244+
getOutMode() {
245+
return "";
246+
},
247+
getActiveTurn() {
248+
return null;
249+
},
250+
getActiveThrows() {
251+
return [];
252+
},
253+
getActiveScore() {
254+
return null;
255+
},
256+
getSnapshot() {
257+
return null;
258+
},
259+
subscribe() {
260+
return () => {};
261+
},
262+
};
263+
264+
const cleanup = startTvBoardZoom({ documentRef, windowRef, gameState });
265+
266+
try {
267+
timers.advance(25);
268+
269+
assert.equal(targetNode.classList.contains(ZOOM_CLASS), true);
270+
assert.equal(hostNode.classList.contains(ZOOM_HOST_CLASS), true);
271+
assert.match(String(targetNode.style.transform || ""), /scale\(/);
272+
} finally {
273+
cleanup();
274+
timers.restoreGlobals();
275+
}
276+
});
277+
230278
test("tv-board-zoom stays inactive on Bull-off even when a stale X01 snapshot remains", async () => {
231279
const documentRef = new FakeDocument();
232280
documentRef.variantElement.textContent = "Bull-off";

0 commit comments

Comments
 (0)