ёбанные удаления надеюсь финальные
This commit is contained in:
3
app.py
3
app.py
@@ -29,7 +29,8 @@ from ui_builder import install_ui_builder
|
|||||||
from khl_site.khl_data_center import APP as khl_site_app
|
from khl_site.khl_data_center import APP as khl_site_app
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
BUILD_VERSION = "2026.08.20.16"
|
BUILD_VERSION = "2026.08.20.17"
|
||||||
|
# compatibility: BUILD_VERSION = "2026.08.20.16"
|
||||||
# compatibility: BUILD_VERSION = "2026.08.20.15"
|
# compatibility: BUILD_VERSION = "2026.08.20.15"
|
||||||
# compatibility: BUILD_VERSION = "2026.08.20.14"
|
# compatibility: BUILD_VERSION = "2026.08.20.14"
|
||||||
# compatibility: BUILD_VERSION = "2026.08.20.13"
|
# compatibility: BUILD_VERSION = "2026.08.20.13"
|
||||||
|
|||||||
@@ -5459,6 +5459,18 @@ class HockeyDataService:
|
|||||||
)
|
)
|
||||||
if not assigned:
|
if not assigned:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# BUILD96: a prepared penalty must not change numerical strength until
|
||||||
|
# the operator explicitly starts it. New Runtime snapshots always send
|
||||||
|
# startedOnce. Legacy snapshots (without the marker) keep the old
|
||||||
|
# assigned-immediately behaviour for backward compatibility.
|
||||||
|
has_started_marker = "startedOnce" in item or "started_once" in item
|
||||||
|
if has_started_marker:
|
||||||
|
started = bool(item.get("startedOnce", item.get("started_once", False))) or bool(item.get("running", False))
|
||||||
|
else:
|
||||||
|
started = bool(item.get("running", False)) or remaining_ms < duration_ms or assigned
|
||||||
|
if not started:
|
||||||
|
continue
|
||||||
if side == "home":
|
if side == "home":
|
||||||
home += 1
|
home += 1
|
||||||
else:
|
else:
|
||||||
@@ -5588,6 +5600,10 @@ class HockeyDataService:
|
|||||||
"penalty_board": {
|
"penalty_board": {
|
||||||
"penalties": [],
|
"penalties": [],
|
||||||
"history": [],
|
"history": [],
|
||||||
|
"advantage_cycle": {
|
||||||
|
"had_advantage": False,
|
||||||
|
"last_advantage_side": "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5625,6 +5641,15 @@ class HockeyDataService:
|
|||||||
history = source_board.get("history")
|
history = source_board.get("history")
|
||||||
penalties = [item for item in penalties if isinstance(item, dict)] if isinstance(penalties, list) else []
|
penalties = [item for item in penalties if isinstance(item, dict)] if isinstance(penalties, list) else []
|
||||||
history = [item for item in history if isinstance(item, dict)] if isinstance(history, list) else []
|
history = [item for item in history if isinstance(item, dict)] if isinstance(history, list) else []
|
||||||
|
source_cycle = source_board.get("advantage_cycle")
|
||||||
|
source_cycle = source_cycle if isinstance(source_cycle, dict) else {}
|
||||||
|
cycle_side = str(source_cycle.get("last_advantage_side") or "").lower()
|
||||||
|
if cycle_side not in {"home", "away"}:
|
||||||
|
cycle_side = ""
|
||||||
|
advantage_cycle = {
|
||||||
|
"had_advantage": bool(source_cycle.get("had_advantage", False)) and bool(cycle_side),
|
||||||
|
"last_advantage_side": cycle_side,
|
||||||
|
}
|
||||||
|
|
||||||
normalised = {
|
normalised = {
|
||||||
"saved": True,
|
"saved": True,
|
||||||
@@ -5638,6 +5663,7 @@ class HockeyDataService:
|
|||||||
"penalty_board": {
|
"penalty_board": {
|
||||||
"penalties": penalties[:64],
|
"penalties": penalties[:64],
|
||||||
"history": history[:64],
|
"history": history[:64],
|
||||||
|
"advantage_cycle": advantage_cycle,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
encoded = json.dumps(normalised, ensure_ascii=False, separators=(",", ":"))
|
encoded = json.dumps(normalised, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|||||||
151
tests/test_build96_penalty_strength_transition_state_machine.py
Normal file
151
tests/test_build96_penalty_strength_transition_state_machine.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from hockey_data.service import HockeyDataService
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||||
|
APP = (ROOT / "app.py").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _penalty(side: str, remaining: int, *, started: bool) -> dict:
|
||||||
|
return {
|
||||||
|
"side": side,
|
||||||
|
"infraction": {"id": "minor"},
|
||||||
|
"preset": "2m",
|
||||||
|
"durationMs": 120000,
|
||||||
|
"remainingMs": remaining,
|
||||||
|
"running": False,
|
||||||
|
"startedOnce": started,
|
||||||
|
"finished": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _strength(*penalties: dict) -> dict:
|
||||||
|
settings = {
|
||||||
|
"strength_regulation_skaters": 5,
|
||||||
|
"strength_min_skaters": 3,
|
||||||
|
"strength_state_labels": {
|
||||||
|
"regulation": {
|
||||||
|
"5x4": {"ru": "БОЛ"},
|
||||||
|
"4x4": {"ru": "4x4"},
|
||||||
|
"4x3": {"ru": "4x3"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return HockeyDataService._strength_payload(
|
||||||
|
settings,
|
||||||
|
stage="regular",
|
||||||
|
current_period="2",
|
||||||
|
timer_state={"penalty_board": {"penalties": list(penalties)}},
|
||||||
|
language="ru",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepared_penalty_does_not_change_strength_until_explicit_start():
|
||||||
|
prepared = _strength(_penalty("home", 120000, started=False))
|
||||||
|
assert prepared["strength_label"] == "5×5"
|
||||||
|
assert prepared["advantage_side"] == ""
|
||||||
|
|
||||||
|
started = _strength(_penalty("home", 120000, started=True))
|
||||||
|
assert started["strength_label"] == "4×5"
|
||||||
|
assert started["advantage_side"] == "away"
|
||||||
|
assert started["state_label"] == "БОЛ"
|
||||||
|
|
||||||
|
|
||||||
|
def test_complex_transition_strength_sequence_matches_requested_flow():
|
||||||
|
# Initial HOME penalty: AWAY/right has the advantage.
|
||||||
|
p1 = _penalty("home", 80000, started=True)
|
||||||
|
value = _strength(p1)
|
||||||
|
assert value["strength_label"] == "4×5"
|
||||||
|
assert value["advantage_side"] == "away"
|
||||||
|
|
||||||
|
# Coincidental pair is added while the first penalty is still running.
|
||||||
|
p2 = _penalty("home", 120000, started=True)
|
||||||
|
p3 = _penalty("away", 120000, started=True)
|
||||||
|
value = _strength(p1, p2, p3)
|
||||||
|
assert value["strength_label"] == "3×4"
|
||||||
|
assert value["advantage_side"] == "away"
|
||||||
|
assert value["state_label"] == "4x3"
|
||||||
|
|
||||||
|
# The first penalty expires: the pair remains, so strength is equal 4x4.
|
||||||
|
value = _strength(
|
||||||
|
_penalty("home", 40000, started=True),
|
||||||
|
_penalty("away", 40000, started=True),
|
||||||
|
)
|
||||||
|
assert value["strength_label"] == "4×4"
|
||||||
|
assert value["advantage_side"] == ""
|
||||||
|
assert value["state_label"] == "4x4"
|
||||||
|
|
||||||
|
# AWAY/right takes another penalty: HOME/left now has a 4x3 advantage.
|
||||||
|
value = _strength(
|
||||||
|
_penalty("home", 30000, started=True),
|
||||||
|
_penalty("away", 30000, started=True),
|
||||||
|
_penalty("away", 120000, started=True),
|
||||||
|
)
|
||||||
|
assert value["strength_label"] == "4×3"
|
||||||
|
assert value["advantage_side"] == "home"
|
||||||
|
assert value["state_label"] == "4x3"
|
||||||
|
|
||||||
|
# Coincidental pair expires, leaving ordinary HOME power play.
|
||||||
|
value = _strength(_penalty("away", 90000, started=True))
|
||||||
|
assert value["strength_label"] == "5×4"
|
||||||
|
assert value["advantage_side"] == "home"
|
||||||
|
assert value["state_label"] == "БОЛ"
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_holds_single_plate_through_temporary_equal_strength():
|
||||||
|
start = APP_JS.index("function penaltyDisplayEntriesByTargetSide")
|
||||||
|
end = APP_JS.index("function hockeyPenaltySideMappingDetail", start)
|
||||||
|
block = APP_JS[start:end]
|
||||||
|
assert "canHoldEqualStrength" in block
|
||||||
|
assert "state.hockeyPenaltyAdvantageCycle.hadAdvantage" in block
|
||||||
|
assert "lastAdvantageSide" in block
|
||||||
|
assert "const canHoldEqualStrength = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage)" in block
|
||||||
|
assert 'home: rememberedSide === "home" && transitionEntry ? [transitionEntry] : []' in block
|
||||||
|
assert 'away: rememberedSide === "away" && transitionEntry ? [transitionEntry] : []' in block
|
||||||
|
assert 'home: advantageSide === "home" && transitionEntry ? [transitionEntry] : []' in block
|
||||||
|
assert 'away: advantageSide === "away" && transitionEntry ? [transitionEntry] : []' in block
|
||||||
|
assert "holdingEqualStrength" in block
|
||||||
|
# The countdown source remains the globally soonest active penalty.
|
||||||
|
assert "const transitionEntry = all[0] || null;" in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_pure_equal_strength_without_previous_advantage_still_has_no_plate():
|
||||||
|
start = APP_JS.index("function penaltyDisplayEntriesByTargetSide")
|
||||||
|
end = APP_JS.index("function hockeyPenaltySideMappingDetail", start)
|
||||||
|
block = APP_JS[start:end]
|
||||||
|
assert "if (!canHoldEqualStrength)" in block
|
||||||
|
assert "return { home: [], away: [], routedToAdvantage: false" in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_advantage_cycle_is_saved_and_restored_for_reload_during_4x4():
|
||||||
|
snapshot = APP_JS.split("function hockeyGameTimerSnapshot", 1)[1].split("function hockeyApplySavedTimers", 1)[0]
|
||||||
|
assert "advantage_cycle" in snapshot
|
||||||
|
assert "had_advantage" in snapshot
|
||||||
|
assert "last_advantage_side" in snapshot
|
||||||
|
restore = APP_JS.split("function hockeyApplySavedTimers", 1)[1].split("async function hockeyPersistGameTimers", 1)[0]
|
||||||
|
assert "restoredPenaltyAdvantageCycle" in restore
|
||||||
|
assert "savedCycle.last_advantage_side" in restore
|
||||||
|
|
||||||
|
normalized = HockeyDataService._normalise_game_timer_state({
|
||||||
|
"penalty_board": {
|
||||||
|
"penalties": [],
|
||||||
|
"history": [],
|
||||||
|
"advantage_cycle": {"had_advantage": True, "last_advantage_side": "away"},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert normalized["penalty_board"]["advantage_cycle"] == {
|
||||||
|
"had_advantage": True,
|
||||||
|
"last_advantage_side": "away",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_strength_waits_for_last_started_penalty_not_prepared_draft():
|
||||||
|
control = APP_JS.split("function controlHockeyPenalty", 1)[1].split("function formatHockeyPenaltyTime", 1)[0]
|
||||||
|
assert "hockeyPenaltyHasStarted(item)" in control
|
||||||
|
ticker = APP_JS.split("function updateHockeyPenaltyBoards", 1)[1].split("function registerHockeyBoardNode", 1)[0]
|
||||||
|
assert "remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item)).length" in ticker
|
||||||
|
|
||||||
|
|
||||||
|
def test_build96_runtime_version():
|
||||||
|
assert 'BUILD_VERSION = "2026.08.20.17"' in APP
|
||||||
@@ -4749,34 +4749,62 @@ function startCustomTooltips() {
|
|||||||
return homeSkaters > awaySkaters ? "home" : awaySkaters > homeSkaters ? "away" : "";
|
return homeSkaters > awaySkaters ? "home" : awaySkaters > homeSkaters ? "away" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BUILD96: one persistent strength-transition plate.
|
||||||
|
// When the game moves from a real advantage into temporary equal strength
|
||||||
|
// (for example 4x5 -> 3x4 -> 4x4), keep the same plate on the team that held
|
||||||
|
// the advantage. Only the configured strength caption and the next transition
|
||||||
|
// countdown change. A pure coincidental 4x4 that did not originate from an
|
||||||
|
// advantage still produces no plate.
|
||||||
function penaltyDisplayEntriesByTargetSide(step) {
|
function penaltyDisplayEntriesByTargetSide(step) {
|
||||||
const home = sortedPenaltyEntries("home");
|
const home = sortedPenaltyEntries("home");
|
||||||
const away = sortedPenaltyEntries("away");
|
const away = sortedPenaltyEntries("away");
|
||||||
const all = [...home, ...away].sort((a, b) => Number(a.event.remainingMs || 0) - Number(b.event.remainingMs || 0)
|
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));
|
|| Number(a.event.createdAt || 0) - Number(b.event.createdAt || 0));
|
||||||
if (!all.length) {
|
if (!all.length) {
|
||||||
return { home: [], away: [], routedToAdvantage: false, advantageSide: "", transitionEntry: null };
|
return { home: [], away: [], routedToAdvantage: false, advantageSide: "", plateSide: "", holdingEqualStrength: false, transitionEntry: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const advantageSide = penaltyLocalAdvantageSide(home.length, away.length);
|
const advantageSide = penaltyLocalAdvantageSide(home.length, away.length);
|
||||||
|
const transitionEntry = all[0] || null;
|
||||||
if (!advantageSide) {
|
if (!advantageSide) {
|
||||||
return { home: [], away: [], routedToAdvantage: false, advantageSide: "", transitionEntry: all[0] || null };
|
const rememberedSide = String(state.hockeyPenaltyAdvantageCycle.lastAdvantageSide || "");
|
||||||
|
const canHoldEqualStrength = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage)
|
||||||
|
&& ["home", "away"].includes(rememberedSide);
|
||||||
|
if (!canHoldEqualStrength) {
|
||||||
|
return { home: [], away: [], routedToAdvantage: false, advantageSide: "", plateSide: "", holdingEqualStrength: false, transitionEntry };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
home: rememberedSide === "home" && transitionEntry ? [transitionEntry] : [],
|
||||||
|
away: rememberedSide === "away" && transitionEntry ? [transitionEntry] : [],
|
||||||
|
routedToAdvantage: false,
|
||||||
|
advantageSide: "",
|
||||||
|
plateSide: rememberedSide,
|
||||||
|
holdingEqualStrength: true,
|
||||||
|
transitionEntry,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionEntry = all[0] || null;
|
|
||||||
return {
|
return {
|
||||||
home: advantageSide === "home" && transitionEntry ? [transitionEntry] : [],
|
home: advantageSide === "home" && transitionEntry ? [transitionEntry] : [],
|
||||||
away: advantageSide === "away" && transitionEntry ? [transitionEntry] : [],
|
away: advantageSide === "away" && transitionEntry ? [transitionEntry] : [],
|
||||||
routedToAdvantage: true,
|
routedToAdvantage: true,
|
||||||
advantageSide,
|
advantageSide,
|
||||||
|
plateSide: advantageSide,
|
||||||
|
holdingEqualStrength: false,
|
||||||
transitionEntry,
|
transitionEntry,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberPenaltyAdvantagePlan(displayPlan) {
|
function rememberPenaltyAdvantagePlan(displayPlan) {
|
||||||
if (!displayPlan?.routedToAdvantage || !["home", "away"].includes(String(displayPlan.advantageSide || ""))) return;
|
if (!displayPlan?.routedToAdvantage || !["home", "away"].includes(String(displayPlan.advantageSide || ""))) return;
|
||||||
|
const nextSide = String(displayPlan.advantageSide);
|
||||||
|
const changed = !state.hockeyPenaltyAdvantageCycle.hadAdvantage
|
||||||
|
|| String(state.hockeyPenaltyAdvantageCycle.lastAdvantageSide || "") !== nextSide;
|
||||||
state.hockeyPenaltyAdvantageCycle.hadAdvantage = true;
|
state.hockeyPenaltyAdvantageCycle.hadAdvantage = true;
|
||||||
state.hockeyPenaltyAdvantageCycle.lastAdvantageSide = String(displayPlan.advantageSide);
|
state.hockeyPenaltyAdvantageCycle.lastAdvantageSide = nextSide;
|
||||||
|
// BUILD96: persist the owner immediately. This matters if Runtime is reloaded
|
||||||
|
// during the following temporary equal-strength phase (for example 4x4).
|
||||||
|
if (changed && !state.hockeyTimerHydrating) hockeyScheduleTimerSave(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetPenaltyAdvantageCycle() {
|
function resetPenaltyAdvantageCycle() {
|
||||||
@@ -6540,12 +6568,15 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
event.id,
|
event.id,
|
||||||
Math.round(Number(event.remainingMs || 0) / 1000),
|
Math.round(Number(event.remainingMs || 0) / 1000),
|
||||||
Boolean(event.running),
|
Boolean(event.running),
|
||||||
|
Boolean(event.startedOnce),
|
||||||
Boolean(event.finished),
|
Boolean(event.finished),
|
||||||
event.preset || "",
|
event.preset || "",
|
||||||
event.player?.id || "",
|
event.player?.id || "",
|
||||||
event.infraction?.id || "",
|
event.infraction?.id || "",
|
||||||
].join(":" )).join(";"),
|
].join(":" )).join(";"),
|
||||||
board.history.map((item) => item.id || "").join(","),
|
board.history.map((item) => item.id || "").join(","),
|
||||||
|
Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage),
|
||||||
|
String(state.hockeyPenaltyAdvantageCycle.lastAdvantageSide || ""),
|
||||||
].join("|");
|
].join("|");
|
||||||
if (
|
if (
|
||||||
force
|
force
|
||||||
@@ -6930,8 +6961,8 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
const fullStrengthSide = penaltyFullStrengthSide();
|
const fullStrengthSide = penaltyFullStrengthSide();
|
||||||
board.penalties = board.penalties.filter((item) => item.id !== event.id);
|
board.penalties = board.penalties.filter((item) => item.id !== event.id);
|
||||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
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 remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
|
||||||
const remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item)).length;
|
const remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item)).length;
|
||||||
const clearCommonSelection = board.selectedEventId === event.id;
|
const clearCommonSelection = board.selectedEventId === event.id;
|
||||||
const clearSideSelection = board.selectedPreviewEventIds?.[side] === event.id;
|
const clearSideSelection = board.selectedPreviewEventIds?.[side] === event.id;
|
||||||
if (clearCommonSelection) board.selectedEventId = null;
|
if (clearCommonSelection) board.selectedEventId = null;
|
||||||
@@ -6950,7 +6981,10 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
had_advantage: hadAdvantage,
|
had_advantage: hadAdvantage,
|
||||||
full_strength_side: fullStrengthSide,
|
full_strength_side: fullStrengthSide,
|
||||||
});
|
});
|
||||||
if (remainingTotal <= 0) resetPenaltyAdvantageCycle();
|
if (remainingTotal <= 0) {
|
||||||
|
resetPenaltyAdvantageCycle();
|
||||||
|
hockeyScheduleTimerSave(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} else if (command === "set_time") {
|
} else if (command === "set_time") {
|
||||||
@@ -6987,7 +7021,10 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
});
|
});
|
||||||
state.vmixPenaltyMirrors.delete(penaltyMirrorKey(component, event));
|
state.vmixPenaltyMirrors.delete(penaltyMirrorKey(component, event));
|
||||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
if (!board.penalties.some((item) => !item.finished && hockeyEventReady(item))) resetPenaltyAdvantageCycle();
|
if (!board.penalties.some((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item))) {
|
||||||
|
resetPenaltyAdvantageCycle();
|
||||||
|
hockeyScheduleTimerSave(true);
|
||||||
|
}
|
||||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }).catch((error) => console.error("Penalty target remove rebalance error", error));
|
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true }).catch((error) => console.error("Penalty target remove rebalance error", error));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -7089,7 +7126,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
const hadAdvantage = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage);
|
const hadAdvantage = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage);
|
||||||
const fullStrengthSide = penaltyFullStrengthSide();
|
const fullStrengthSide = penaltyFullStrengthSide();
|
||||||
const remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item)).length;
|
const remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item)).length;
|
||||||
const finishBySide = new Map();
|
const finishBySide = new Map();
|
||||||
completedEvents.forEach((event) => {
|
completedEvents.forEach((event) => {
|
||||||
const side = String(event.player?.side || event.side || "").toLowerCase();
|
const side = String(event.player?.side || event.side || "").toLowerCase();
|
||||||
@@ -7099,7 +7136,7 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
.catch((error) => console.error("Penalty target ticker rebalance error", error))
|
.catch((error) => console.error("Penalty target ticker rebalance error", error))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
finishBySide.forEach((event, side) => {
|
finishBySide.forEach((event, side) => {
|
||||||
const remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
|
const remainingOnSide = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item) && String(item.player?.side || item.side || "").toLowerCase() === side).length;
|
||||||
fireConfiguredTimerFinishActions("penalty", {
|
fireConfiguredTimerFinishActions("penalty", {
|
||||||
side, component, event,
|
side, component, event,
|
||||||
remaining_on_side: remainingOnSide,
|
remaining_on_side: remainingOnSide,
|
||||||
@@ -7108,7 +7145,10 @@ function openTimerQuickEditor(focusActionId = "") {
|
|||||||
full_strength_side: fullStrengthSide,
|
full_strength_side: fullStrengthSide,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (remainingTotal <= 0) resetPenaltyAdvantageCycle();
|
if (remainingTotal <= 0) {
|
||||||
|
resetPenaltyAdvantageCycle();
|
||||||
|
hockeyScheduleTimerSave(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -11573,6 +11613,9 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
durationMs: Math.max(0, Math.round(Number(event.durationMs || 0))),
|
durationMs: Math.max(0, Math.round(Number(event.durationMs || 0))),
|
||||||
remainingMs: Math.max(0, Math.round(Number(event.remainingMs || 0))),
|
remainingMs: Math.max(0, Math.round(Number(event.remainingMs || 0))),
|
||||||
running: Boolean(event.running) && !Boolean(event.finished),
|
running: Boolean(event.running) && !Boolean(event.finished),
|
||||||
|
startedOnce: Boolean(event.startedOnce || event.running)
|
||||||
|
|| (Math.max(0, Number(event.durationMs || 0)) > 0
|
||||||
|
&& Math.max(0, Number(event.remainingMs ?? event.durationMs ?? 0)) < Math.max(0, Number(event.durationMs || 0))),
|
||||||
finished: Boolean(event.finished),
|
finished: Boolean(event.finished),
|
||||||
readyEmitted: Boolean(event.readyEmitted),
|
readyEmitted: Boolean(event.readyEmitted),
|
||||||
assignedEmitted: Boolean(event.assignedEmitted),
|
assignedEmitted: Boolean(event.assignedEmitted),
|
||||||
@@ -11611,6 +11654,12 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
penalty_board: {
|
penalty_board: {
|
||||||
penalties: (board?.penalties || []).slice(0, 64).map(hockeyPenaltySnapshot),
|
penalties: (board?.penalties || []).slice(0, 64).map(hockeyPenaltySnapshot),
|
||||||
history: (board?.history || []).slice(0, 64).map(hockeyHistorySnapshot),
|
history: (board?.history || []).slice(0, 64).map(hockeyHistorySnapshot),
|
||||||
|
advantage_cycle: {
|
||||||
|
had_advantage: Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage),
|
||||||
|
last_advantage_side: ["home", "away"].includes(String(state.hockeyPenaltyAdvantageCycle.lastAdvantageSide || ""))
|
||||||
|
? String(state.hockeyPenaltyAdvantageCycle.lastAdvantageSide)
|
||||||
|
: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -11644,11 +11693,22 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
updateTimerNodes(mainComponent, timerState);
|
updateTimerNodes(mainComponent, timerState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let restoredPenaltyAdvantageCycle = null;
|
||||||
const boardComponent = hockeyPenaltyBoardComponent();
|
const boardComponent = hockeyPenaltyBoardComponent();
|
||||||
if (boardComponent) {
|
if (boardComponent) {
|
||||||
const source = timers?.penalty_board && typeof timers.penalty_board === "object"
|
const source = timers?.penalty_board && typeof timers.penalty_board === "object"
|
||||||
? timers.penalty_board
|
? timers.penalty_board
|
||||||
: {};
|
: {};
|
||||||
|
const savedCycle = source?.advantage_cycle && typeof source.advantage_cycle === "object"
|
||||||
|
? source.advantage_cycle
|
||||||
|
: null;
|
||||||
|
if (savedCycle) {
|
||||||
|
const savedSide = String(savedCycle.last_advantage_side || "");
|
||||||
|
restoredPenaltyAdvantageCycle = {
|
||||||
|
hadAdvantage: Boolean(savedCycle.had_advantage) && ["home", "away"].includes(savedSide),
|
||||||
|
lastAdvantageSide: ["home", "away"].includes(savedSide) ? savedSide : "",
|
||||||
|
};
|
||||||
|
}
|
||||||
const board = createHockeyBoardState(boardComponent);
|
const board = createHockeyBoardState(boardComponent);
|
||||||
board.penalties = (Array.isArray(source.penalties) ? source.penalties : [])
|
board.penalties = (Array.isArray(source.penalties) ? source.penalties : [])
|
||||||
.slice(0, 64)
|
.slice(0, 64)
|
||||||
@@ -11664,7 +11724,12 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
|||||||
refreshHockeyBoardNodes(boardComponent);
|
refreshHockeyBoardNodes(boardComponent);
|
||||||
}
|
}
|
||||||
state.hockeyTimerGameId = String(gameId || "");
|
state.hockeyTimerGameId = String(gameId || "");
|
||||||
resetPenaltyAdvantageCycle();
|
if (restoredPenaltyAdvantageCycle) {
|
||||||
|
state.hockeyPenaltyAdvantageCycle.hadAdvantage = Boolean(restoredPenaltyAdvantageCycle.hadAdvantage);
|
||||||
|
state.hockeyPenaltyAdvantageCycle.lastAdvantageSide = String(restoredPenaltyAdvantageCycle.lastAdvantageSide || "");
|
||||||
|
} else {
|
||||||
|
resetPenaltyAdvantageCycle();
|
||||||
|
}
|
||||||
state.hockeyPenaltyMappingContextSignature = "";
|
state.hockeyPenaltyMappingContextSignature = "";
|
||||||
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
hockeySyncPenaltySideMappingContext({ force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user