поправлены сложные удаления

This commit is contained in:
2026-08-20 15:06:17 +03:00
parent 559888cc0c
commit 3dc21e4619
6 changed files with 109 additions and 44 deletions

View File

@@ -4635,42 +4635,35 @@ function startCustomTooltips() {
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
}
// BUILD85: HOME/AWAY penalty targets describe the TEAM THAT HAS THE NUMERICAL
// ADVANTAGE, not the bench that committed the penalty. This matters not only
// for a normal 5x4/5x3 power play, but also for 4x3 and 3x4 when both benches
// still have active penalties. The authoritative advantage_side is calculated
// by the hockey strength engine. When strength is equal (4x4 / 3x3), keep the
// traditional one-per-side coincidental display. If the strength payload has
// not arrived yet, the single-penalty fallback still routes to the opposite
// target exactly as BUILD83 did.
// BUILD88: complex/coincidental penalties must not immediately switch the
// scorebug extension into a power-play side merely because the raw skater
// count is 4x3/3x4. While both benches still have active penalties, keep one
// penalty on each team's own target. Only after one bench becomes completely
// clear does the remaining penalty move to the opposite target (the team that
// is now actually shown as playing on the power play). If both benches clear
// together, the plan is empty and both extra plates are removed.
function penaltyDisplayEntriesByTargetSide(step) {
const home = sortedPenaltyEntries("home");
const away = sortedPenaltyEntries("away");
const soonestOnly = String(step?.penalty_display_mode || "soonest") !== "all";
const strength = getByPath(state.data, "hockey.game_control.strength") || {};
const advantageSide = String(strength.advantage_side || "").trim().toLowerCase();
const take = (items) => soonestOnly ? items.slice(0, 1) : items;
if (advantageSide === "home") {
// HOME has more skaters, therefore an AWAY penalty controls the PP clock.
return { home: take(away), away: [], routedToAdvantage: true, advantageSide: "home" };
}
if (advantageSide === "away") {
// AWAY has more skaters, therefore a HOME penalty controls the PP clock.
return { home: [], away: take(home), routedToAdvantage: true, advantageSide: "away" };
}
// Complex/coincidental state: do not announce a power-play side yet.
if (home.length && away.length) {
return { home: take(home), away: take(away), routedToAdvantage: false, advantageSide: "" };
}
// Only HOME still has a penalty -> AWAY is the displayed PP side.
if (home.length) {
if (soonestOnly) return { home: [], away: home.slice(0, 1), routedToAdvantage: true };
return { home: [], away: home, routedToAdvantage: true, advantageSide: "away" };
return { home: [], away: take(home), routedToAdvantage: true, advantageSide: "away" };
}
// Only AWAY still has a penalty -> HOME is the displayed PP side.
if (away.length) {
if (soonestOnly) return { home: away.slice(0, 1), away: [], routedToAdvantage: true };
return { home: away, away: [], routedToAdvantage: true, advantageSide: "home" };
return { home: take(away), away: [], routedToAdvantage: true, advantageSide: "home" };
}
// Both sides cleared at the same tick: show nothing.
return { home: [], away: [], routedToAdvantage: false, advantageSide: "" };
}
@@ -11136,12 +11129,10 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|| hockeyStrengthMappingSignature(previousControl) !== hockeyStrengthMappingSignature(payload);
if (strengthChanged) {
hockeyRefreshVmixMappingForStrength(gameId, previousControl, payload).catch(() => {});
// BUILD87: penalty routing depends on the recalculated numerical strength.
// The local penalty editor can rebalance a few milliseconds before the
// timer save response arrives, while state.data still contains the old
// 4x4/5x5 strength. Re-run the routing as soon as the authoritative
// control payload is stored so 2 penalties vs 1 immediately becomes
// 3x4/4x3 and the plate moves to the team with the advantage.
// BUILD88: keep a second rebalance after the authoritative control payload
// arrives. The display plan itself intentionally keeps HOME/AWAY on their
// own sides while both benches have active penalties; once one bench clears,
// this refresh moves the remaining timer to the actual power-play side.
if (state.activeHockeyVmixTimerSteps.size) {
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
.catch((error) => console.error("Penalty strength rebalance error", error));
@@ -13288,7 +13279,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
<div class="shortcut-finish-action-list">${timerFinishActionRows(step)}</div>
<div class="shortcut-vmix-inventory-note"><span>${escapeHtml(shortcutInventoryLabel())}</span><small>Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.</small></div>
<p class="shortcut-step-note">Режим <b>Text mirror</b> рекомендуется: веб-таймер является источником истины и раз в секунду отправляет <code>SetText</code> строго в выбранные <code>Input + SelectedName</code>. Режим <b>Countdown</b> оставлен для титров, где countdown уже настроен внутри vMix. Для верхнего счёта по умолчанию используется одно ближайшее к окончанию удаление. Пока удаления есть у обеих команд, HOME/AWAY показываются по своим сторонам. Когда штраф остаётся только у одной команды, его таймер автоматически переезжает на Input противоположной команды — стороны большинства. Режим «Все удаления по слотам» оставлен как дополнительный. Действие по окончании показывает выбранный Input в заданном Overlay и автоматически убирает его через указанное время.</p>
<p class="shortcut-step-note">Режим <b>Text mirror</b> рекомендуется: веб-таймер является источником истины и раз в секунду отправляет <code>SetText</code> строго в выбранные <code>Input + SelectedName</code>. Режим <b>Countdown</b> оставлен для титров, где countdown уже настроен внутри vMix. Для верхнего счёта по умолчанию используется одно ближайшее к окончанию удаление. При сложных/обоюдных удалениях, пока штрафы есть у обеих команд, HOME/AWAY остаются на своих сторонах и режим «играют в большинстве» не включается. Только когда одна сторона полностью очистится, оставшийся таймер переезжает на Input противоположной команды — стороны большинства. Если обе стороны очистились одновременно, дополнительные плашки просто снимаются. Режим «Все удаления по слотам» оставлен как дополнительный. Действие по окончании показывает выбранный Input в заданном Overlay и автоматически убирает его через указанное время.</p>
</div>`;
} else if (step.type === "delay") {
body.innerHTML = `<div class="shortcut-step-grid"><label>Задержка, мс<input type="number" min="0" max="10000" step="10" data-step-field="milliseconds" value="${Number(step.milliseconds) || 0}"></label></div>`;