diff --git a/static/css/custom_scoring_panel.css b/static/css/custom_scoring_panel.css new file mode 100644 index 00000000..2f755ade --- /dev/null +++ b/static/css/custom_scoring_panel.css @@ -0,0 +1,12 @@ +/* Custom-game scoring panel styles. Loaded in addition to the shared scoring_panel.css, only by + templates/custom_scoring_panel.html.tmpl — these selectors target classes the custom panel emits + (.status-toggle / .status-control), so they're kept out of the shared stylesheet. */ + +.status-toggle[data-selected="true"] { + background-color: var(--alliance-highlight); +} + +.status-control .team-num { + color: var(--text-active); + font-weight: bold; +} diff --git a/static/css/referee_panel.css b/static/css/referee_panel.css index 0629edaa..fd3e464b 100644 --- a/static/css/referee_panel.css +++ b/static/css/referee_panel.css @@ -279,3 +279,17 @@ h3 { #scoreSummary .tower-status[data-status="3"] { background-color: #2a8c55; } + +#scoreSummary .status-badge { + min-width: 4.8vw; + padding: 0.05vw 0.2vw; + border-radius: 0.25vw; + color: #fff; + font-weight: bold; + white-space: nowrap; + background-color: #555; +} + +#scoreSummary .status-badge[data-active="true"] { + background-color: #2a8c55; +} diff --git a/static/css/scoring_panel.css b/static/css/scoring_panel.css index 4b60d621..9fe9655c 100644 --- a/static/css/scoring_panel.css +++ b/static/css/scoring_panel.css @@ -154,7 +154,9 @@ body { padding: 0; width: 100%; max-width: none; - height: 100%; + /* min-height (not height) so the alliance background covers the full content when the scoring + elements overflow one viewport, instead of leaving the below-the-fold rows on the page default. */ + min-height: 100%; display: flex; flex-direction: column; justify-content: flex-start; diff --git a/static/img/blinds-logo.png b/static/img/blinds-logo.png index 84936e5c..784f5108 100644 Binary files a/static/img/blinds-logo.png and b/static/img/blinds-logo.png differ diff --git a/static/img/game-logo.png b/static/img/game-logo.png index 509f93cb..784f5108 100644 Binary files a/static/img/game-logo.png and b/static/img/game-logo.png differ diff --git a/static/img/mayhem.png b/static/img/mayhem.png new file mode 100644 index 00000000..784f5108 Binary files /dev/null and b/static/img/mayhem.png differ diff --git a/static/js/audience_display.js b/static/js/audience_display.js index 2a839bcd..467ae938 100644 --- a/static/js/audience_display.js +++ b/static/js/audience_display.js @@ -12,6 +12,11 @@ var websocket; let transitionMap; const transitionQueue = []; let transitionInProgress = false; + +// Set to true by generated_audience_display.js when it loads (custom game mode only). Explicit +// equivalent of the Go-side "if game.CustomGameMode" branch, rather than feature-detecting via +// typeof on a function that may not be declared at all in the FRC build. +let IS_CUSTOM_GAME_MODE = false; let currentScreen = "blank"; let redSide; let blueSide; @@ -762,10 +767,18 @@ $(function () { handlePlaySound(event.data); }, realtimeScore: function (event) { - handleRealtimeScore(event.data); + if (IS_CUSTOM_GAME_MODE) { + handleRealtimeScoreGenerated(event.data); + } else { + handleRealtimeScore(event.data); + } }, scorePosted: function (event) { - handleScorePosted(event.data); + if (IS_CUSTOM_GAME_MODE) { + handleScorePostedGenerated(event.data); + } else { + handleScorePosted(event.data); + } }, }); diff --git a/static/js/custom_audience_display.js.tmpl b/static/js/custom_audience_display.js.tmpl new file mode 100644 index 00000000..b4a6760a --- /dev/null +++ b/static/js/custom_audience_display.js.tmpl @@ -0,0 +1,95 @@ +// Code generated by cmd/generate from game/custom_game.yaml. DO NOT EDIT. +// Regenerate: go generate ./... + +// Loading this file at all means custom game mode is active; flip the flag declared by the +// shared audience_display.js so it dispatches to the generated handlers below explicitly. +IS_CUSTOM_GAME_MODE = true; + +const handleRealtimeScoreGenerated = function (data) { + $("#leftScoreNumber").text(data.Red.ScoreSummary.Score); + $("#rightScoreNumber").text(data.Blue.ScoreSummary.Score); + +[[range .ScoringGroups]] $(`#left[[camel .ID]]Count`).text([[range $i, $f := .CountFields]][[if $i]] + [[end]](data.Red.Score.[[$f]] || 0)[[end]]); + $(`#right[[camel .ID]]Count`).text([[range $i, $f := .CountFields]][[if $i]] + [[end]](data.Blue.Score.[[$f]] || 0)[[end]]); +[[end]]}; + +const handleScorePostedGenerated = function (data) { + if (data.RedWon) { + setFinalResultIndicator(redSide, "WINNER", "winner"); + setFinalResultIndicator(blueSide, "", ""); + } else if (data.BlueWon) { + setFinalResultIndicator(redSide, "", ""); + setFinalResultIndicator(blueSide, "WINNER", "winner"); + } else { + setFinalResultIndicator(redSide, "TIE", "tie"); + setFinalResultIndicator(blueSide, "TIE", "tie"); + } + const tiebreakReason = data.TiebreakReason || ""; + $("#finalTiebreakReason").text(tiebreakReason); + $("#finalTiebreakReason").attr("data-visible", tiebreakReason !== ""); + + $("#leftFinalScore").text(data.RedScoreSummary.Score); + $("#leftFinalAlliance").text("Alliance " + data.Match.PlayoffRedAlliance); + setTeamInfo("left", 1, data.Match.Red1, data.RedCards, data.RedRankings); + setTeamInfo("left", 2, data.Match.Red2, data.RedCards, data.RedRankings); + setTeamInfo("left", 3, data.Match.Red3, data.RedCards, data.RedRankings); + if (data.RedOffFieldTeamIds.length > 0) { + setTeamInfo("left", 4, data.RedOffFieldTeamIds[0], data.RedCards, data.RedRankings); + } else { + setTeamInfo("left", 4, 0, data.RedCards, data.RedRankings); + } + + $("#rightFinalScore").text(data.BlueScoreSummary.Score); + $("#rightFinalAlliance").text("Alliance " + data.Match.PlayoffBlueAlliance); + setTeamInfo("right", 1, data.Match.Blue1, data.BlueCards, data.BlueRankings); + setTeamInfo("right", 2, data.Match.Blue2, data.BlueCards, data.BlueRankings); + setTeamInfo("right", 3, data.Match.Blue3, data.BlueCards, data.BlueRankings); + if (data.BlueOffFieldTeamIds.length > 0) { + setTeamInfo("right", 4, data.BlueOffFieldTeamIds[0], data.BlueCards, data.BlueRankings); + } else { + setTeamInfo("right", 4, 0, data.BlueCards, data.BlueRankings); + } + + // Populate Red (left) breakdown points +[[range .ScoringGroups]] $(`#leftFinal[[camel .ID]]Points`).text(data.RedScoreSummary.[[camel .ID]]Points || 0); +[[end]][[range .Statuses]] $(`#leftFinal[[camel .ID]]Points`).text(data.RedScoreSummary.[[camel .ID]]Points); +[[end]] $(`#leftFinalFoulPoints`).text(data.RedScoreSummary.FoulPoints); +[[range .RankingPoints]] $(`#leftFinal[[camel .ID]]RankingPoint`).html(data.RedScoreSummary.[[camel .ID]]RankingPoint ? "✔" : "✘"); + $(`#leftFinal[[camel .ID]]RankingPoint`).attr('data-checked', data.RedScoreSummary.[[camel .ID]]RankingPoint); +[[end]] $(`#leftFinalRankingPoints`).html(data.RedRankingPoints); + $(`#leftFinalWins`).text(data.RedWins); + const redFinalDestination = $(`#leftFinalDestination`); + redFinalDestination.html(data.RedDestination.replace("Advances to ", "Advances to
")); + redFinalDestination.toggle(data.RedDestination !== ""); + redFinalDestination.attr("data-won", data.RedWon); + + // Populate Blue (right) breakdown points +[[range .ScoringGroups]] $(`#rightFinal[[camel .ID]]Points`).text(data.BlueScoreSummary.[[camel .ID]]Points || 0); +[[end]][[range .Statuses]] $(`#rightFinal[[camel .ID]]Points`).text(data.BlueScoreSummary.[[camel .ID]]Points); +[[end]] $(`#rightFinalFoulPoints`).text(data.BlueScoreSummary.FoulPoints); +[[range .RankingPoints]] $(`#rightFinal[[camel .ID]]RankingPoint`).html(data.BlueScoreSummary.[[camel .ID]]RankingPoint ? "✔" : "✘"); + $(`#rightFinal[[camel .ID]]RankingPoint`).attr('data-checked', data.BlueScoreSummary.[[camel .ID]]RankingPoint); +[[end]] $(`#rightFinalRankingPoints`).html(data.BlueRankingPoints); + $(`#rightFinalWins`).text(data.BlueWins); + const blueFinalDestination = $(`#rightFinalDestination`); + blueFinalDestination.html(data.BlueDestination.replace("Advances to ", "Advances to
")); + blueFinalDestination.toggle(data.BlueDestination !== ""); + blueFinalDestination.attr("data-won", data.BlueWon); + + let matchName = data.Match.LongName; + if (data.Match.NameDetail !== "") { + matchName += " – " + data.Match.NameDetail; + } + $("#finalMatchName").html(matchName); + + // Reload the bracket to reflect any changes. + $("#bracketSvg").attr("src", "/api/bracket/svg?activeMatch=saved&v=" + new Date().getTime()); + + if (data.Match.Type === matchTypePlayoff) { + $(".playoff-hidden-field").hide(); + $(".playoff-only-field").show(); + } else { + $(".playoff-hidden-field").show(); + $(".playoff-only-field").hide(); + } +}; diff --git a/static/js/custom_referee_panel.js.tmpl b/static/js/custom_referee_panel.js.tmpl new file mode 100644 index 00000000..9b0d2ea6 --- /dev/null +++ b/static/js/custom_referee_panel.js.tmpl @@ -0,0 +1,23 @@ +// Code generated by cmd/generate from game/custom_game.yaml. DO NOT EDIT. +// Regenerate: go generate ./... + +// Loading this file at all means custom game mode is active; flip the flag declared by the +// shared referee_panel.js so it dispatches to the generated handler below explicitly. +IS_CUSTOM_GAME_MODE = true; + +const updateScoreSummaryGenerated = function (scoreRoot, score) { +[[range .Phases]][[if .Counts]] $(`#${scoreRoot} .phase-[[.Name]]`).text([[jsScoreArray .CountFields]].join(" / ")); +[[end]][[end]][[range .Statuses]][[if .IsBool]] $(`#${scoreRoot} .team-1-[[.ID]]`).text(score.[[camel .ID]]Statuses[0] ? "\u2713" : "\u2717"); + $(`#${scoreRoot} .team-1-[[.ID]]`).attr('data-active', !!score.[[camel .ID]]Statuses[0]); + $(`#${scoreRoot} .team-2-[[.ID]]`).text(score.[[camel .ID]]Statuses[1] ? "\u2713" : "\u2717"); + $(`#${scoreRoot} .team-2-[[.ID]]`).attr('data-active', !!score.[[camel .ID]]Statuses[1]); + $(`#${scoreRoot} .team-3-[[.ID]]`).text(score.[[camel .ID]]Statuses[2] ? "\u2713" : "\u2717"); + $(`#${scoreRoot} .team-3-[[.ID]]`).attr('data-active', !!score.[[camel .ID]]Statuses[2]); +[[else]] const [[camel .ID]]Names = [[jsStrings (displayNames .Values)]]; + $(`#${scoreRoot} .team-1-[[.ID]]`).text([[camel .ID]]Names[score.[[camel .ID]]Statuses[0]]); + $(`#${scoreRoot} .team-1-[[.ID]]`).attr('data-active', score.[[camel .ID]]Statuses[0] !== 0); + $(`#${scoreRoot} .team-2-[[.ID]]`).text([[camel .ID]]Names[score.[[camel .ID]]Statuses[1]]); + $(`#${scoreRoot} .team-2-[[.ID]]`).attr('data-active', score.[[camel .ID]]Statuses[1] !== 0); + $(`#${scoreRoot} .team-3-[[.ID]]`).text([[camel .ID]]Names[score.[[camel .ID]]Statuses[2]]); + $(`#${scoreRoot} .team-3-[[.ID]]`).attr('data-active', score.[[camel .ID]]Statuses[2] !== 0); +[[end]][[end]]}; diff --git a/static/js/custom_scoring_panel.js.tmpl b/static/js/custom_scoring_panel.js.tmpl new file mode 100644 index 00000000..f2ee00aa --- /dev/null +++ b/static/js/custom_scoring_panel.js.tmpl @@ -0,0 +1,56 @@ +// Code generated by cmd/generate from game/custom_game.yaml. DO NOT EDIT. +// Regenerate: go generate ./... + +// Loading this file at all means custom game mode is active; flip the flag declared by the +// shared scoring_panel.js so it dispatches to the generated handler below explicitly. +IS_CUSTOM_GAME_MODE = true; + +const boolStatuses = {}; + +const adjustCount = function (id, phase, delta) { + websocket.send("adjustCount", {Id: id, Phase: phase, Delta: delta}); +}; + +const toggleBoolStatus = function (id, robotIndex) { + const key = id + "-" + robotIndex; + const current = !!boolStatuses[key]; + websocket.send("setStatus", {Id: id, RobotIndex: robotIndex, Value: !current}); +}; + +const setEnumStatus = function (id, robotIndex, valueId) { + websocket.send("setEnumStatus", {Id: id, RobotIndex: robotIndex, ValueId: valueId}); +}; + +const cycleEnumStatus = function (id, robotIndex) { + websocket.send("cycleEnumStatus", {Id: id, RobotIndex: robotIndex}); +}; + +[[range .Statuses]][[if not .IsBool]]const [[camel .ID]]Names = [[jsStrings (displayNames .Values)]]; +[[end]][[end]] +const handleRealtimeScoreGenerated = function (data) { + let score; + if (alliance === "red") { + score = data.Red.Score; + } else { + score = data.Blue.Score; + } + + // Update counts +[[range .ScoringCounts]][[range .Phases]] $(`#[[.ID]]-[[.Phase]]-count`).text(score.[[phasePrefix .Phase]][[camel .ID]]Count); +[[end]][[end]] + // Update statuses +[[range .Statuses]][[if .IsBool]] for (let i = 0; i < 3; i++) { + const val = !!score.[[camel .ID]]Statuses[i]; + boolStatuses['[[.ID]]-' + i] = val; + $("#[[.ID]]-" + i).attr('data-selected', val); + } +[[else]] for (let i = 0; i < 3; i++) { + const idx = score.[[camel .ID]]Statuses[i]; + $(`#[[.ID]]-${i}`).text([[camel .ID]]Names[idx]); + $(`#[[.ID]]-${i}`).attr('data-selected', idx !== 0); + } +[[end]][[end]] + const redFouls = data.Red.Score.Fouls || []; + const blueFouls = data.Blue.Score.Fouls || []; + renderGlobalFoulCounts(redFouls, blueFouls); +}; diff --git a/static/js/referee_panel.js b/static/js/referee_panel.js index 1bd53433..b757b200 100644 --- a/static/js/referee_panel.js +++ b/static/js/referee_panel.js @@ -9,6 +9,11 @@ let blueFoulsHashCode = 0; let scoreIsReady = false; let isPostMatch = false; +// Set to true by generated_referee_panel.js when it loads (custom game mode only). Explicit +// equivalent of the Go-side "if game.CustomGameMode" branch, rather than feature-detecting via +// typeof on a function that may not be declared at all in the FRC build. +let IS_CUSTOM_GAME_MODE = false; + // Sends the foul to the server to add it to the list. const addFoul = function (alliance, isMajor) { websocket.send("addFoul", {Alliance: alliance, IsMajor: isMajor}); @@ -169,12 +174,16 @@ const handleRealtimeScore = function (data) { } let scoreRoot = `${alliance}ScoreSummary`; - setTowerStatus(`#${scoreRoot} .team-1-auto-tower`, score.AutoTowerStatuses[0]); - setTowerStatus(`#${scoreRoot} .team-2-auto-tower`, score.AutoTowerStatuses[1]); - setTowerStatus(`#${scoreRoot} .team-3-auto-tower`, score.AutoTowerStatuses[2]); - setTowerStatus(`#${scoreRoot} .team-1-endgame-tower`, score.EndgameTowerStatuses[0]); - setTowerStatus(`#${scoreRoot} .team-2-endgame-tower`, score.EndgameTowerStatuses[1]); - setTowerStatus(`#${scoreRoot} .team-3-endgame-tower`, score.EndgameTowerStatuses[2]); + if (IS_CUSTOM_GAME_MODE) { + updateScoreSummaryGenerated(scoreRoot, score); + } else { + setTowerStatus(`#${scoreRoot} .team-1-auto-tower`, score.AutoTowerStatuses[0]); + setTowerStatus(`#${scoreRoot} .team-2-auto-tower`, score.AutoTowerStatuses[1]); + setTowerStatus(`#${scoreRoot} .team-3-auto-tower`, score.AutoTowerStatuses[2]); + setTowerStatus(`#${scoreRoot} .team-1-endgame-tower`, score.EndgameTowerStatuses[0]); + setTowerStatus(`#${scoreRoot} .team-2-endgame-tower`, score.EndgameTowerStatuses[1]); + setTowerStatus(`#${scoreRoot} .team-3-endgame-tower`, score.EndgameTowerStatuses[2]); + } } } diff --git a/static/js/scoring_panel.js b/static/js/scoring_panel.js index e5a46d8d..e02262fe 100644 --- a/static/js/scoring_panel.js +++ b/static/js/scoring_panel.js @@ -8,6 +8,11 @@ var websocket; let alliance; let committed = false; +// Set to true by generated_scoring_panel.js when it loads (custom game mode only). Explicit +// equivalent of the Go-side "if game.CustomGameMode" branch, rather than feature-detecting via +// typeof on a function that may not be declared at all in the FRC build. +let IS_CUSTOM_GAME_MODE = false; + // True when scoring controls in general should be available let scoringAvailable = false; // True when the commit button should be available @@ -177,7 +182,11 @@ $(function () { handleMatchTime(event.data); }, realtimeScore: function (event) { - handleRealtimeScore(event.data); + if (IS_CUSTOM_GAME_MODE) { + handleRealtimeScoreGenerated(event.data); + } else { + handleRealtimeScore(event.data); + } }, resetLocalState: function (event) { resetLocalState(); diff --git a/templates/custom_audience_display.html.tmpl b/templates/custom_audience_display.html.tmpl new file mode 100644 index 00000000..a2f05705 --- /dev/null +++ b/templates/custom_audience_display.html.tmpl @@ -0,0 +1,278 @@ + + + + Audience Display - {{.EventSettings.Name}} - Cheesy Arena + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+[[range .ScoringGroups]]
+ [[.DisplayName]] + 0 +
+[[end]]
+
+
+
+
+
+[[range .ScoringGroups]]
+ [[.DisplayName]] + 0 +
+[[end]]
+
+ + + +
+
+
+
+
+
+
+
+
+ {{.EventSettings.Name}} + +
+
+
+
+
+  -  +
+
+ +
+
+
+
+
+ Next Up:
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{range $i := seq 4}} +
+ +
+
+
+
+
+ +
+
+
+ {{end}} +
+
+
+
+
+
+[[range .ScoringGroups]]
0
+[[end]][[range .Statuses]]
0
+[[end]]
0
+
+[[range .RankingPoints]]
+[[end]]
0
+
+
+
 
+
0
+
+
+
+[[range .ScoringGroups]]
[[.DisplayName]]
+[[end]][[range .Statuses]]
[[.DisplayName]]
+[[end]]
Foul
+
+[[range .RankingPoints]]
[[.DisplayName]]
+[[end]]
Ranking Points
+
+
+
 
+
Wins
+
+
+
+[[range .ScoringGroups]]
0
+[[end]][[range .Statuses]]
0
+[[end]]
0
+
+[[range .RankingPoints]]
+[[end]]
0
+
+
+
 
+
0
+
+
+
+
+
+ {{range $i := seq 4}} +
+ +
+
+
+
+
+ +
+
+
+ {{end}} +
+
+
+
+
+
+
+ + +
+
+
+
+
+ +
+ +
+ + +
+ +
+
+
+
+ + + + {{range $sound := .MatchSounds}} + + {{end}} + + + + + + + + + + + + diff --git a/templates/custom_referee_panel.html.tmpl b/templates/custom_referee_panel.html.tmpl new file mode 100644 index 00000000..51a4524e --- /dev/null +++ b/templates/custom_referee_panel.html.tmpl @@ -0,0 +1,112 @@ +{{define "title"}}Custom Referee Panel{{end}} +{{define "body"}} +
+
+
+

Red/Yellow Cards

+
+ {{range $i := seq 3}} + {{template "teamCard" dict "alliance" "red" "position" $i}} + {{end}} +
+
+ {{range $i := seq 3}} + {{template "teamCard" dict "alliance" "blue" "position" $i}} + {{end}} +
+
+
+
+
+
+
+
+ {{template "scoreSummary" dict "id" "blueScoreSummary"}} + {{template "scoreSummary" dict "id" "redScoreSummary"}} +
+

Fouls

+
+
Blue
+
Blue Major
+
Red
+
Red Major
+
+
+
+
+

Note: Team and rule assignment are optional.

+
+
Signal Count
+
Signal Reset
+
Commit & Post
+
+ + +{{end}} +{{define "head"}} + + + +{{end}} +{{define "script"}} + + + +{{end}} +{{define "teamCard"}} +
+
+{{end}} +{{define "scoreSummary"}} +
+
+
0
+
0
+
0
+[[range .Phases]][[if .Counts]] +
[[.Title]]
+
[[range $i, $c := .Counts]][[if $i]] / [[end]]0[[end]]
+[[end]][[range .Statuses]] +
[[.DisplayName]]
+
-
+
-
+
-
+[[end]][[end]]
+{{end}} diff --git a/templates/custom_scoring_panel.html.tmpl b/templates/custom_scoring_panel.html.tmpl new file mode 100644 index 00000000..266d0d98 --- /dev/null +++ b/templates/custom_scoring_panel.html.tmpl @@ -0,0 +1,83 @@ +{{define "title"}}Custom Scoring Panel{{end}} +{{define "head"}} + + + + +{{end}} +{{define "body"}} + +
+
+[[range .Phases]]
+

[[.Title]]

+
+[[range .Counts]]
+ [[.DisplayName]] +
+ + 0 + +
+
+[[end]][[range .Statuses]][[if .IsBool]]
+ [[.DisplayName]] +
+ {{range $i := seq 3}} +
+ + +
+ {{end}} +
+
+[[else]]
+ [[.DisplayName]] +
+ {{range $i := seq 3}} +
+ + +
+ {{end}} +
+
+[[end]][[end]]
+
+[[end]]
+
+ + +
+
+ + +
+
Add Fouls
+
+ {{template "foulButton" (dict "id" "foul-blue-minor" "color" "blue" "label" "Blue" "isMajor" false)}} + {{template "foulButton" (dict "id" "foul-red-minor" "color" "red" "label" "Red" "isMajor" false)}} + {{template "foulButton" (dict "id" "foul-blue-major" "color" "blue" "label" "Blue Major" "isMajor" true)}} + {{template "foulButton" (dict "id" "foul-red-major" "color" "red" "label" "Red Major" "isMajor" true)}} +
+ +
+
+{{end}} + +{{define "script"}} + + + +{{end}} + +{{define "foulButton"}} + +{{end}}