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 @@
+
+
+
+
+
+
+
+
+
+ Note: Team and rule assignment are optional.
+