поправлены в какой то раз удаления для верхнего счета
This commit is contained in:
@@ -84,6 +84,7 @@
|
||||
vmixPenaltyMirrors: new Map(),
|
||||
activeHockeyVmixTimerSteps: new Set(),
|
||||
vmixPenaltyTargetAssignments: new Map(),
|
||||
hockeyPenaltyAdvantageCycle: { hadAdvantage: false, lastAdvantageSide: "" },
|
||||
hockeyPenaltyMappingContextSignature: "",
|
||||
hockeyPenaltyMappingContextPending: false,
|
||||
hockeyPenaltyMappingContextQueued: false,
|
||||
@@ -4635,36 +4636,76 @@ function startCustomTooltips() {
|
||||
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
||||
}
|
||||
|
||||
// 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.
|
||||
// BUILD89: the scorebug has only ONE penalty/power-play plate at a time.
|
||||
// The plate belongs to the team that currently has the numerical advantage.
|
||||
// Its timer is the next strength-transition timer: the shortest remaining
|
||||
// active penalty across BOTH benches. Pure coincidental/equal strength shows
|
||||
// no penalty plate at all. When that shortest timer expires we recalculate and
|
||||
// keep the same single plate with the next timer if an advantage still exists.
|
||||
function penaltyLocalAdvantageSide(homeCount, awayCount) {
|
||||
const strength = getByPath(state.data, "hockey.game_control.strength") || {};
|
||||
const authoritativeHome = Number(strength.home_penalties);
|
||||
const authoritativeAway = Number(strength.away_penalties);
|
||||
if (Number.isFinite(authoritativeHome) && Number.isFinite(authoritativeAway)
|
||||
&& authoritativeHome === homeCount && authoritativeAway === awayCount) {
|
||||
const side = String(strength.advantage_side || "");
|
||||
if (side === "home" || side === "away") return side;
|
||||
return "";
|
||||
}
|
||||
|
||||
const base = Math.max(3, Math.min(6, Number(strength.base_skaters || 5)));
|
||||
const minimum = Math.max(2, Math.min(base, Number(strength.minimum_skaters || 3)));
|
||||
const mode = String(strength.penalty_mode || "subtract");
|
||||
let homeSkaters;
|
||||
let awaySkaters;
|
||||
if (mode === "add_opponent") {
|
||||
homeSkaters = Math.min(5, base + awayCount);
|
||||
awaySkaters = Math.min(5, base + homeCount);
|
||||
} else {
|
||||
homeSkaters = Math.max(minimum, base - homeCount);
|
||||
awaySkaters = Math.max(minimum, base - awayCount);
|
||||
}
|
||||
return homeSkaters > awaySkaters ? "home" : awaySkaters > homeSkaters ? "away" : "";
|
||||
}
|
||||
|
||||
function penaltyDisplayEntriesByTargetSide(step) {
|
||||
const home = sortedPenaltyEntries("home");
|
||||
const away = sortedPenaltyEntries("away");
|
||||
const soonestOnly = String(step?.penalty_display_mode || "soonest") !== "all";
|
||||
const take = (items) => soonestOnly ? items.slice(0, 1) : items;
|
||||
|
||||
// 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: "" };
|
||||
const all = [...home, ...away].sort((a, b) => Number(a.event.remainingMs || 0) - Number(b.event.remainingMs || 0)
|
||||
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
||||
if (!all.length) {
|
||||
return { home: [], away: [], routedToAdvantage: false, advantageSide: "", transitionEntry: null };
|
||||
}
|
||||
|
||||
// Only HOME still has a penalty -> AWAY is the displayed PP side.
|
||||
if (home.length) {
|
||||
return { home: [], away: take(home), routedToAdvantage: true, advantageSide: "away" };
|
||||
const advantageSide = penaltyLocalAdvantageSide(home.length, away.length);
|
||||
if (!advantageSide) {
|
||||
return { home: [], away: [], routedToAdvantage: false, advantageSide: "", transitionEntry: all[0] || null };
|
||||
}
|
||||
|
||||
// Only AWAY still has a penalty -> HOME is the displayed PP side.
|
||||
if (away.length) {
|
||||
return { home: take(away), away: [], routedToAdvantage: true, advantageSide: "home" };
|
||||
}
|
||||
const transitionEntry = all[0] || null;
|
||||
return {
|
||||
home: advantageSide === "home" && transitionEntry ? [transitionEntry] : [],
|
||||
away: advantageSide === "away" && transitionEntry ? [transitionEntry] : [],
|
||||
routedToAdvantage: true,
|
||||
advantageSide,
|
||||
transitionEntry,
|
||||
};
|
||||
}
|
||||
|
||||
// Both sides cleared at the same tick: show nothing.
|
||||
return { home: [], away: [], routedToAdvantage: false, advantageSide: "" };
|
||||
function rememberPenaltyAdvantagePlan(displayPlan) {
|
||||
if (!displayPlan?.routedToAdvantage || !["home", "away"].includes(String(displayPlan.advantageSide || ""))) return;
|
||||
state.hockeyPenaltyAdvantageCycle.hadAdvantage = true;
|
||||
state.hockeyPenaltyAdvantageCycle.lastAdvantageSide = String(displayPlan.advantageSide);
|
||||
}
|
||||
|
||||
function resetPenaltyAdvantageCycle() {
|
||||
state.hockeyPenaltyAdvantageCycle.hadAdvantage = false;
|
||||
state.hockeyPenaltyAdvantageCycle.lastAdvantageSide = "";
|
||||
}
|
||||
|
||||
function penaltyFullStrengthSide() {
|
||||
const advantage = String(state.hockeyPenaltyAdvantageCycle.lastAdvantageSide || "");
|
||||
return advantage === "home" ? "away" : advantage === "away" ? "home" : "";
|
||||
}
|
||||
|
||||
function hockeyPenaltySideMappingDetail(item, side) {
|
||||
@@ -4750,7 +4791,9 @@ function startCustomTooltips() {
|
||||
}
|
||||
|
||||
async function rebalanceVmixPenaltyTargets({ force = false, hideUnused = true } = {}) {
|
||||
const commands = [];
|
||||
const outCommands = [];
|
||||
const setCommands = [];
|
||||
const inCommands = [];
|
||||
const assignedMirrorKeys = new Set();
|
||||
for (const stepId of Array.from(state.activeHockeyVmixTimerSteps)) {
|
||||
const step = hockeyTimerSyncStepById(stepId);
|
||||
@@ -4760,6 +4803,7 @@ function startCustomTooltips() {
|
||||
}
|
||||
if (step.penalty_vmix_mode !== "text") continue;
|
||||
const displayPlan = penaltyDisplayEntriesByTargetSide(step);
|
||||
rememberPenaltyAdvantagePlan(displayPlan);
|
||||
for (const side of ["home", "away"]) {
|
||||
const allTargets = sequencePenaltyTargets(step, side);
|
||||
const soonestOnly = String(step.penalty_display_mode || "soonest") !== "all";
|
||||
@@ -4790,7 +4834,7 @@ function startCustomTooltips() {
|
||||
const value = formatHockeyPenaltyTime(entry.event.remainingMs);
|
||||
const mirror = state.vmixPenaltyMirrors.get(eventKey);
|
||||
if (force || !mirror || mirror.lastValue !== value || previous?.eventKey !== eventKey) {
|
||||
commands.push({ Function: "SetText", Input: target.input, SelectedName: target.selected_name, Value: value });
|
||||
setCommands.push({ Function: "SetText", Input: target.input, SelectedName: target.selected_name, Value: value });
|
||||
if (mirror) mirror.lastValue = value;
|
||||
}
|
||||
|
||||
@@ -4804,15 +4848,15 @@ function startCustomTooltips() {
|
||||
if (!targetWasVisible) {
|
||||
if (previous?.input && (String(previous.input) !== String(target.input) || String(previous.overlay || overlay) !== overlay)) {
|
||||
const previousOverlay = ["1", "2", "3", "4"].includes(String(previous.overlay || "")) ? String(previous.overlay) : overlay;
|
||||
commands.push({ Function: `OverlayInput${previousOverlay}Out`, Input: previous.input });
|
||||
outCommands.push({ Function: `OverlayInput${previousOverlay}Out`, Input: previous.input });
|
||||
}
|
||||
commands.push({ Function: `OverlayInput${overlay}In`, Input: target.input });
|
||||
inCommands.push({ Function: `OverlayInput${overlay}In`, Input: target.input });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (previous && hideUnused && target.auto_hide_on_finish !== false && target.input) {
|
||||
const overlay = ["1", "2", "3", "4"].includes(String(target.overlay || "")) ? String(target.overlay) : "2";
|
||||
commands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input });
|
||||
outCommands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input });
|
||||
}
|
||||
state.vmixPenaltyTargetAssignments.delete(assignmentKey);
|
||||
}
|
||||
@@ -4822,7 +4866,7 @@ function startCustomTooltips() {
|
||||
const previous = state.vmixPenaltyTargetAssignments.get(assignmentKey);
|
||||
if (previous && hideUnused && target.auto_hide_on_finish !== false && target.input) {
|
||||
const overlay = ["1", "2", "3", "4"].includes(String(target.overlay || "")) ? String(target.overlay) : "2";
|
||||
commands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input });
|
||||
outCommands.push({ Function: `OverlayInput${overlay}Out`, Input: target.input });
|
||||
}
|
||||
state.vmixPenaltyTargetAssignments.delete(assignmentKey);
|
||||
});
|
||||
@@ -4833,6 +4877,10 @@ function startCustomTooltips() {
|
||||
state.vmixPenaltyMirrors.delete(mirrorKey);
|
||||
}
|
||||
}
|
||||
// Always take the old single plate OUT before putting the new side IN.
|
||||
// HOME and AWAY targets frequently share the same Overlay slot; sending IN
|
||||
// first and OUT second would remove the newly selected plate.
|
||||
const commands = [...outCommands, ...setCommands, ...inCommands];
|
||||
if (commands.length) await sendRuntimeVmixSequence(commands);
|
||||
return commands.length;
|
||||
}
|
||||
@@ -4875,7 +4923,16 @@ function startCustomTooltips() {
|
||||
if (source === "game" && String(step.game_timer_action_id || "hockey_game_timer") !== gameActionId) return;
|
||||
normalizeTimerFinishActions(step.timer_finish_actions).forEach((action) => {
|
||||
if (!finishActionMatchesSource(action, source, side)) return;
|
||||
if (source === "penalty" && action.only_when_side_clear !== false && Number(meta.remaining_on_side || 0) > 0) return;
|
||||
if (source === "penalty" && action.only_when_side_clear !== false) {
|
||||
// BUILD89: the full-strength/final plate is a FINAL transition only.
|
||||
// Do not fire it when a coincidental timer ends while another penalty
|
||||
// is still active, and do not fire it after a purely coincidental
|
||||
// sequence that never produced a numerical advantage.
|
||||
if (Number(meta.remaining_total || 0) > 0) return;
|
||||
if (!Boolean(meta.had_advantage)) return;
|
||||
const fullStrengthSide = String(meta.full_strength_side || "");
|
||||
if (fullStrengthSide && side !== fullStrengthSide) return;
|
||||
}
|
||||
actions.push(action);
|
||||
});
|
||||
});
|
||||
@@ -4946,6 +5003,7 @@ function startCustomTooltips() {
|
||||
|
||||
if (step.sync_vmix_penalties) {
|
||||
const displayPlan = penaltyDisplayEntriesByTargetSide(step);
|
||||
rememberPenaltyAdvantagePlan(displayPlan);
|
||||
const soonestOnly = String(step.penalty_display_mode || "soonest") !== "all";
|
||||
const activeHomeTargets = soonestOnly ? homeTargets.slice(0, 1) : homeTargets;
|
||||
const activeAwayTargets = soonestOnly ? awayTargets.slice(0, 1) : awayTargets;
|
||||
@@ -6734,9 +6792,12 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
...hockeyPenaltyContext(event)
|
||||
});
|
||||
state.vmixPenaltyMirrors.delete(penaltyMirrorKey(component, event));
|
||||
const hadAdvantage = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage);
|
||||
const fullStrengthSide = penaltyFullStrengthSide();
|
||||
board.penalties = board.penalties.filter((item) => item.id !== event.id);
|
||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
||||
const remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
|
||||
const remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item)).length;
|
||||
const clearCommonSelection = board.selectedEventId === event.id;
|
||||
const clearSideSelection = board.selectedPreviewEventIds?.[side] === event.id;
|
||||
if (clearCommonSelection) board.selectedEventId = null;
|
||||
@@ -6747,7 +6808,16 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
|
||||
.catch((error) => console.error("Penalty target finish rebalance error", error))
|
||||
.finally(() => fireConfiguredTimerFinishActions("penalty", { side, component, event, remaining_on_side: remainingOnSide }));
|
||||
.finally(() => {
|
||||
fireConfiguredTimerFinishActions("penalty", {
|
||||
side, component, event,
|
||||
remaining_on_side: remainingOnSide,
|
||||
remaining_total: remainingTotal,
|
||||
had_advantage: hadAdvantage,
|
||||
full_strength_side: fullStrengthSide,
|
||||
});
|
||||
if (remainingTotal <= 0) resetPenaltyAdvantageCycle();
|
||||
});
|
||||
return true;
|
||||
} else if (command === "set_time") {
|
||||
event.remainingMs = Math.max(0, parseTimerMilliseconds(rawValue, event.remainingMs));
|
||||
@@ -6783,6 +6853,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
});
|
||||
state.vmixPenaltyMirrors.delete(penaltyMirrorKey(component, event));
|
||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||
if (!board.penalties.some((item) => !item.finished && hockeyEventReady(item))) resetPenaltyAdvantageCycle();
|
||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }).catch((error) => console.error("Penalty target remove rebalance error", error));
|
||||
return true;
|
||||
}
|
||||
@@ -6878,6 +6949,9 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
persistHockeyBoard(component, board, true);
|
||||
refreshHockeyBoardNodes(component);
|
||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||
const hadAdvantage = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage);
|
||||
const fullStrengthSide = penaltyFullStrengthSide();
|
||||
const remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item)).length;
|
||||
const finishBySide = new Map();
|
||||
completedEvents.forEach((event) => {
|
||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
||||
@@ -6888,8 +6962,15 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
.finally(() => {
|
||||
finishBySide.forEach((event, side) => {
|
||||
const remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
|
||||
fireConfiguredTimerFinishActions("penalty", { side, component, event, remaining_on_side: remainingOnSide });
|
||||
fireConfiguredTimerFinishActions("penalty", {
|
||||
side, component, event,
|
||||
remaining_on_side: remainingOnSide,
|
||||
remaining_total: remainingTotal,
|
||||
had_advantage: hadAdvantage,
|
||||
full_strength_side: fullStrengthSide,
|
||||
});
|
||||
});
|
||||
if (remainingTotal <= 0) resetPenaltyAdvantageCycle();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -11129,10 +11210,9 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
|| hockeyStrengthMappingSignature(previousControl) !== hockeyStrengthMappingSignature(payload);
|
||||
if (strengthChanged) {
|
||||
hockeyRefreshVmixMappingForStrength(gameId, previousControl, payload).catch(() => {});
|
||||
// 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.
|
||||
// BUILD89: keep a second rebalance after the authoritative control payload
|
||||
// arrives. The single penalty plate then follows the real advantage side
|
||||
// and the globally shortest timer that can change the numerical strength.
|
||||
if (state.activeHockeyVmixTimerSteps.size) {
|
||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
|
||||
.catch((error) => console.error("Penalty strength rebalance error", error));
|
||||
@@ -11441,6 +11521,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
refreshHockeyBoardNodes(boardComponent);
|
||||
}
|
||||
state.hockeyTimerGameId = String(gameId || "");
|
||||
resetPenaltyAdvantageCycle();
|
||||
state.hockeyPenaltyMappingContextSignature = "";
|
||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user