mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 21:56:50 -04:00
Implement scoring panel web interface for 2019.
This commit is contained in:
@@ -10,7 +10,6 @@ import "github.com/Team254/cheesy-arena/game"
|
||||
type RealtimeScore struct {
|
||||
CurrentScore game.Score
|
||||
Cards map[string]string
|
||||
AutoCommitted bool
|
||||
TeleopCommitted bool
|
||||
FoulsCommitted bool
|
||||
}
|
||||
|
||||
@@ -88,17 +88,6 @@
|
||||
padding-left: 20px;
|
||||
text-indent: -20px;
|
||||
}
|
||||
.scoring {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.scoring-comment {
|
||||
font-size: 20px;
|
||||
}
|
||||
.scoring-message {
|
||||
color: #f00;
|
||||
}
|
||||
.btn-lower-third {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ html {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
body {
|
||||
background-color: #333;
|
||||
background-color: #222;
|
||||
padding: 50px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
224
static/css/scoring_panel.css
Normal file
224
static/css/scoring_panel.css
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
Copyright 2019 Team 254. All Rights Reserved.
|
||||
Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
*/
|
||||
html {
|
||||
height: 100%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: #222;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
#matchName {
|
||||
font-size: 2vw;
|
||||
}
|
||||
#robots {
|
||||
margin-bottom: 0.5vw;
|
||||
display: flex;
|
||||
font-size: 1.5vw;
|
||||
color: #ccc;
|
||||
}
|
||||
#robotHeader {
|
||||
margin-right: 1vw;
|
||||
color: #666;
|
||||
}
|
||||
.robot-field {
|
||||
min-width: 12vw;
|
||||
height: 2.5vw;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0.4vw 0.2vw;
|
||||
}
|
||||
.team {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.robot-start-level[data-value="0"] {
|
||||
background-color: #633;
|
||||
}
|
||||
.robot-start-level[data-value="1"] {
|
||||
background-color: #236;
|
||||
}
|
||||
.robot-start-level[data-value="2"] {
|
||||
background-color: #263;
|
||||
}
|
||||
.robot-start-level[data-value="3"] {
|
||||
background-color: #850;
|
||||
}
|
||||
.sandstorm-bonus[data-value="false"] {
|
||||
background-color: #333;
|
||||
}
|
||||
.sandstorm-bonus[data-value="true"] {
|
||||
background-color: #263;
|
||||
}
|
||||
.robot-end-level[data-value="0"] {
|
||||
background-color: #333;
|
||||
}
|
||||
.robot-end-level[data-value="1"] {
|
||||
background-color: #236;
|
||||
}
|
||||
.robot-end-level[data-value="2"] {
|
||||
background-color: #224d4d;
|
||||
}
|
||||
.robot-end-level[data-value="3"] {
|
||||
background-color: #263;
|
||||
}
|
||||
.robot-shortcut {
|
||||
width: 2vw;
|
||||
margin: 0 0.2vw;
|
||||
font-size: 1vw;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.team {
|
||||
font-weight: bold;
|
||||
}
|
||||
#scoringElements {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.rocket {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.rocket-outline {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 5vw 2vw 1vw 2vw;
|
||||
border: 1px solid #222;
|
||||
border-radius: 40% 40% 0% 0%;
|
||||
}
|
||||
.alliance-color[data-alliance="red"] {
|
||||
background-color: #633;
|
||||
}
|
||||
.alliance-color[data-alliance="blue"] {
|
||||
background-color: #236;
|
||||
}
|
||||
.outer-rocket {
|
||||
height: 26vw;
|
||||
margin: 0.2vw;
|
||||
}
|
||||
.inner-rocket {
|
||||
margin: 0.2vw;
|
||||
}
|
||||
#centerColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
#cargoShipContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
#cargoShip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1vw;
|
||||
border: 1px solid #222;
|
||||
border-radius: 10%;
|
||||
}
|
||||
.cargo-ship-side {
|
||||
width: 25vw;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cargo-ship-front {
|
||||
display: flex;
|
||||
}
|
||||
.bay {
|
||||
position: relative;
|
||||
width: 7vw;
|
||||
height: 7vw;
|
||||
margin: 0.2vw;
|
||||
border: 1px solid #222;
|
||||
background-color: #666;
|
||||
border-radius: 10%;
|
||||
}
|
||||
.bay[data-value="0"] .hatch-panel {
|
||||
display: none;
|
||||
}
|
||||
.bay[data-value="1"] .hatch-panel {
|
||||
display: flex;
|
||||
}
|
||||
.bay[data-value="2"] .hatch-panel {
|
||||
display: flex;
|
||||
}
|
||||
.bay[data-value="3"] .hatch-panel {
|
||||
display: none;
|
||||
}
|
||||
.bay[data-value="0"] .cargo {
|
||||
display: none;
|
||||
}
|
||||
.bay[data-value="1"] .cargo {
|
||||
display: none;
|
||||
}
|
||||
.bay[data-value="2"] .cargo {
|
||||
display: flex;
|
||||
}
|
||||
.bay[data-value="3"] .cargo {
|
||||
display: flex;
|
||||
}
|
||||
.shortcut {
|
||||
position: absolute;
|
||||
left: 0.3vw;
|
||||
top: -0.1vw;
|
||||
font-size: 1.2vw;
|
||||
color: #fff
|
||||
}
|
||||
.hatch-panel {
|
||||
width: 6vw;
|
||||
height: 6vw;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
margin: auto auto;
|
||||
border: 1vw solid #c80;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.cargo {
|
||||
width: 3vw;
|
||||
height: 3vw;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
margin: auto auto;
|
||||
background-color: #c50;
|
||||
border-radius: 50%;
|
||||
}
|
||||
#instructions {
|
||||
margin-top: 0.3vw;
|
||||
}
|
||||
#preMatchMessage, #postMatchMessage {
|
||||
height: 100%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
font-size: 1.5vw;
|
||||
color: #c90;
|
||||
}
|
||||
#commitMatchScore {
|
||||
height: 100%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
}
|
||||
#commitMatchScore>button {
|
||||
font-size: 1vw;
|
||||
}
|
||||
@@ -4,41 +4,66 @@
|
||||
// Client-side logic for the scoring interface.
|
||||
|
||||
var websocket;
|
||||
var scoreCommitted = false;
|
||||
var alliance;
|
||||
|
||||
// Handles a websocket message to update the teams for the current match.
|
||||
var handleMatchLoad = function(data) {
|
||||
$("#matchName").text(data.MatchType + " " + data.Match.DisplayName);
|
||||
if (alliance === "red") {
|
||||
$("#team1").text(data.Match.Red1);
|
||||
$("#team2").text(data.Match.Red2);
|
||||
$("#team3").text(data.Match.Red3);
|
||||
} else {
|
||||
$("#team1").text(data.Match.Blue1);
|
||||
$("#team2").text(data.Match.Blue2);
|
||||
$("#team3").text(data.Match.Blue3);
|
||||
}
|
||||
};
|
||||
|
||||
// Handles a websocket message to update the realtime scoring fields.
|
||||
var handleRealtimeScore = function(data) {
|
||||
var realtimeScore;
|
||||
var score;
|
||||
if (alliance === "red") {
|
||||
realtimeScore = data.Red.RealtimeScore;
|
||||
score = data.Red.Score;
|
||||
} else {
|
||||
realtimeScore = data.Blue.RealtimeScore;
|
||||
score = data.Blue.Score;
|
||||
}
|
||||
|
||||
// Update autonomous period values.
|
||||
var score = realtimeScore.CurrentScore;
|
||||
$("#autoRuns").text(score.AutoRuns);
|
||||
$("#climbs").text(score.Climbs);
|
||||
$("#parks").text(score.Parks);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var i1 = i + 1;
|
||||
$("#robotStartLevel" + i1 + ">.value").text(getRobotStartLevelText(score.RobotStartLevels[i]));
|
||||
$("#robotStartLevel" + i1).attr("data-value", score.RobotStartLevels[i]);
|
||||
$("#sandstormBonus" + i1 + ">.value").text(score.SandstormBonuses[i] ? "Yes" : "No");
|
||||
$("#sandstormBonus" + i1).attr("data-value", score.SandstormBonuses[i]);
|
||||
$("#robotEndLevel" + i1 + ">.value").text(getRobotEndLevelText(score.RobotEndLevels[i]));
|
||||
$("#robotEndLevel" + i1).attr("data-value", score.RobotEndLevels[i]);
|
||||
getBay("rocketNearLeft", i).attr("data-value", score.RocketNearLeftBays[i]);
|
||||
getBay("rocketNearRight", i).attr("data-value", score.RocketNearRightBays[i]);
|
||||
getBay("rocketFarLeft", i).attr("data-value", score.RocketFarLeftBays[i]);
|
||||
getBay("rocketFarRight", i).attr("data-value", score.RocketFarRightBays[i]);
|
||||
}
|
||||
for (var i = 0; i < 8; i++) {
|
||||
getBay("cargoShip", i).attr("data-value", score.CargoBays[i]);
|
||||
}
|
||||
};
|
||||
|
||||
// Update component visibility.
|
||||
if (!realtimeScore.AutoCommitted) {
|
||||
$("#autoScoring").fadeTo(0, 1);
|
||||
$("#teleopScoring").hide();
|
||||
$("#waitingMessage").hide();
|
||||
scoreCommitted = false;
|
||||
} else if (!realtimeScore.TeleopCommitted) {
|
||||
$("#autoScoring").fadeTo(0, 0.25);
|
||||
$("#teleopScoring").show();
|
||||
$("#waitingMessage").hide();
|
||||
scoreCommitted = false;
|
||||
} else {
|
||||
$("#autoScoring").hide();
|
||||
$("#teleopScoring").hide();
|
||||
$("#commitMatchScore").hide();
|
||||
$("#waitingMessage").show();
|
||||
scoreCommitted = true;
|
||||
// Handles a websocket message to update the match status.
|
||||
var handleMatchTime = function(data) {
|
||||
switch (matchStates[data.MatchState]) {
|
||||
case "PRE_MATCH":
|
||||
$("#preMatchMessage").css("display", "flex");
|
||||
$("#postMatchMessage").hide();
|
||||
$("#commitMatchScore").hide();
|
||||
break;
|
||||
case "POST_MATCH":
|
||||
$("#preMatchMessage").hide();
|
||||
$("#postMatchMessage").hide();
|
||||
$("#commitMatchScore").css("display", "flex");
|
||||
break;
|
||||
default:
|
||||
$("#preMatchMessage").hide();
|
||||
$("#postMatchMessage").hide();
|
||||
$("#commitMatchScore").hide();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,25 +72,58 @@ var handleKeyPress = function(event) {
|
||||
websocket.send(String.fromCharCode(event.keyCode));
|
||||
};
|
||||
|
||||
// Handles a websocket message to update the match status.
|
||||
var handleMatchTime = function(data) {
|
||||
if (matchStates[data.MatchState] === "POST_MATCH" && !scoreCommitted) {
|
||||
$("#commitMatchScore").show();
|
||||
} else {
|
||||
$("#commitMatchScore").hide();
|
||||
}
|
||||
// Handles an element click and sends the appropriate websocket message.
|
||||
var handleClick = function(shortcut) {
|
||||
websocket.send(shortcut);
|
||||
};
|
||||
|
||||
// Sends a websocket message to indicate that the score for this alliance is ready.
|
||||
var commitMatchScore = function() {
|
||||
websocket.send("commitMatch");
|
||||
$("#postMatchMessage").css("display", "flex");
|
||||
$("#commitMatchScore").hide();
|
||||
};
|
||||
|
||||
// Returns the display text corresponding to the given integer start level value.
|
||||
var getRobotStartLevelText = function(level) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "1";
|
||||
case 2:
|
||||
return "2";
|
||||
case 3:
|
||||
return "No-Show";
|
||||
default:
|
||||
return " ";
|
||||
}
|
||||
};
|
||||
|
||||
// Returns the display text corresponding to the given integer end level value.
|
||||
var getRobotEndLevelText = function(level) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "1";
|
||||
case 2:
|
||||
return "2";
|
||||
case 3:
|
||||
return "3";
|
||||
default:
|
||||
return "Not On";
|
||||
}
|
||||
};
|
||||
|
||||
// Returns the bay element matching the given parameters.
|
||||
var getBay = function(type, index) {
|
||||
return $("#bay" + bayMappings[type][index]);
|
||||
}
|
||||
|
||||
$(function() {
|
||||
alliance = window.location.href.split("/").slice(-1)[0];
|
||||
$(".alliance-color").attr("data-alliance", alliance);
|
||||
|
||||
// Set up the websocket back to the server.
|
||||
websocket = new CheesyWebsocket("/panels/scoring/" + alliance + "/websocket", {
|
||||
matchLoad: function(event) { handleMatchLoad(event.data); },
|
||||
matchTime: function(event) { handleMatchTime(event.data); },
|
||||
realtimeScore: function(event) { handleRealtimeScore(event.data); }
|
||||
});
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
{{"{{/eachMapEntry}}"}}
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "head"}}
|
||||
{{end}}
|
||||
{{define "script"}}
|
||||
<script src="/static/js/match_timing.js"></script>
|
||||
<script src="/static/js/announcer_display.js"></script>
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
<html>
|
||||
<head>
|
||||
{{template "head_common" .}}
|
||||
{{template "head" .}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
@@ -6,80 +6,103 @@
|
||||
*/}}
|
||||
{{define "title"}}Scoring Panel{{end}}
|
||||
{{define "body"}}
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="text-center" id="waitingMessage" style="display: none;">
|
||||
<h3>Waiting for the next match...</h3>
|
||||
<div id="matchName"> </div>
|
||||
<div id="robots">
|
||||
<div id="robotHeader">
|
||||
<div class="robot-field"> </div>
|
||||
<div class="robot-field">Start Hab Level</div>
|
||||
<div class="robot-field">Sandstorm Bonus?</div>
|
||||
<div class="robot-field">End Hab Level</div>
|
||||
</div>
|
||||
<div id="autoScoring" class="col-lg-12 well well-{{.Alliance}}" style="display: none;">
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<h2>Autonomous Period</h2>
|
||||
<p>Use the following keyboard shortcuts:</p>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-lg-offset-1 scoring">r/R</div>
|
||||
<div class="col-lg-8 scoring-comment">Auto runs +/-</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-lg-offset-1 scoring">Enter</div>
|
||||
<div class="col-lg-8 scoring-comment">Commit autonomous score</div>
|
||||
</div>
|
||||
{{range $i := seq 3}}
|
||||
<div>
|
||||
<div id="team{{$i}}" class="team robot-field"></div>
|
||||
<div id="robotStartLevel{{$i}}" class="robot-start-level robot-field" onclick="handleClick('{{$i}}');">
|
||||
<div class="robot-shortcut">{{$i}}</div>
|
||||
<div class="value"></div>
|
||||
<div class="robot-shortcut"></div>
|
||||
</div>
|
||||
<div id="sandstormBonus{{$i}}" class="sandstorm-bonus robot-field" onclick="handleClick('{{add $i 3}}');">
|
||||
<div class="robot-shortcut">{{add $i 3}}</div>
|
||||
<div class="value"></div>
|
||||
<div class="robot-shortcut"></div>
|
||||
</div>
|
||||
<div id="robotEndLevel{{$i}}" class="robot-end-level robot-field" onclick="handleClick('{{add $i 6}}');">
|
||||
<div class="robot-shortcut">{{add $i 6}}</div>
|
||||
<div class="value"></div>
|
||||
<div class="robot-shortcut"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<h2>Autonomous Score</h2>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Auto runs</div>
|
||||
<div class="col-lg-2 scoring-comment" id="autoRuns"></div>
|
||||
</div>
|
||||
<h3 class="text-center scoring-message">Press Enter to commit autonomous score</h3>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="scoringElements">
|
||||
<div class="rocket">
|
||||
<div class="rocket-outline alliance-color">
|
||||
<div class="outer-rocket">{{template "rocketHalf" dict "startBayId" 0 "vars" $}}</div>
|
||||
<div class="inner-rocket">{{template "rocketHalf" dict "startBayId" 3 "vars" $}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="teleopScoring" class="col-lg-12 well well-{{.Alliance}}" style="display: none;">
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<h2>Teleoperated Period</h2>
|
||||
<p>Use the following keyboard shortcuts:</p>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-lg-offset-1 scoring">c/C</div>
|
||||
<div class="col-lg-8 scoring-comment">Climbs +/- (actual; disregard Levitate)</div>
|
||||
<div id="centerColumn">
|
||||
<div id="cargoShipContainer">
|
||||
<div id="cargoShip" class="alliance-color">
|
||||
<div class="cargo-ship-side">
|
||||
{{template "bay" dict "id" 6 "vars" $}}{{template "bay" dict "id" 13 "vars" $}}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-lg-offset-1 scoring">p/P</div>
|
||||
<div class="col-lg-8 scoring-comment">Parks +/-</div>
|
||||
<div class="cargo-ship-side">
|
||||
{{template "bay" dict "id" 7 "vars" $}}{{template "bay" dict "id" 12 "vars" $}}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-lg-offset-1 scoring">a</div>
|
||||
<div class="col-lg-8 scoring-comment">Back to autonomous</div>
|
||||
<div class="cargo-ship-side">
|
||||
{{template "bay" dict "id" 8 "vars" $}}{{template "bay" dict "id" 11 "vars" $}}
|
||||
</div>
|
||||
<div class="cargo-ship-front">
|
||||
{{template "bay" dict "id" 9 "vars" $}}{{template "bay" dict "id" 10 "vars" $}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div>
|
||||
<h2>Teleoperated Score</h2>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Climbs</div>
|
||||
<div class="col-lg-2 scoring-comment" id="climbs"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-lg-offset-1 scoring-comment">Parks</div>
|
||||
<div class="col-lg-2 scoring-comment" id="parks"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="instructions">Click or use the labeled keyboard shortcuts to toggle each element</div>
|
||||
<div id="preMatchMessage">Set pre-match state of robots and cargo ship</div>
|
||||
<div id="commitMatchScore">
|
||||
<button type="button" class="btn btn-success" onclick="commitMatchScore();">
|
||||
Commit Final Match Score
|
||||
</button>
|
||||
</div>
|
||||
<div id="postMatchMessage">Waiting for the next match...</div>
|
||||
</div>
|
||||
<div class="text-center col-lg-12">
|
||||
<button type="button" class="btn btn-info" id="commitMatchScore" onclick="commitMatchScore();"
|
||||
style="display: none;">Commit Final Match Score</button>
|
||||
<div class="rocket">
|
||||
<div class="rocket-outline alliance-color">
|
||||
<div class="inner-rocket">{{template "rocketHalf" dict "startBayId" 14 "vars" $}}</div>
|
||||
<div class="outer-rocket">{{template "rocketHalf" dict "startBayId" 17 "vars" $}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "head"}}
|
||||
<link href="/static/css/scoring_panel.css" rel="stylesheet">
|
||||
{{end}}
|
||||
{{define "script"}}
|
||||
<script>
|
||||
var alliance = "{{.Alliance}}";
|
||||
</script>
|
||||
<script src="/static/js/match_timing.js"></script>
|
||||
<script src="/static/js/scoring_panel.js"></script>
|
||||
<script>
|
||||
var bayMappings = {"cargoShip": [], "rocketNearLeft": [], "rocketNearRight": [], "rocketFarLeft": [],
|
||||
"rocketFarRight": []};
|
||||
{{range $mapping := .BayMappings}}
|
||||
{{if eq $.Alliance "red"}}
|
||||
bayMappings["{{$mapping.RedElement}}"][{{$mapping.RedIndex}}] = {{$mapping.BayId}};
|
||||
{{else}}
|
||||
bayMappings["{{$mapping.BlueElement}}"][{{$mapping.BlueIndex}}] = {{$mapping.BayId}};
|
||||
{{end}}
|
||||
{{end}}
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "rocketHalf"}}
|
||||
{{template "bay" dict "id" .startBayId "vars" .vars}}
|
||||
{{template "bay" dict "id" (add .startBayId 1) "vars" .vars}}
|
||||
{{template "bay" dict "id" (add .startBayId 2) "vars" .vars}}
|
||||
{{end}}
|
||||
{{define "bay"}}
|
||||
<div id="bay{{.id}}" class="bay" onclick="handleClick('{{(index .vars.BayMappings .id).Shortcut}}');">
|
||||
<div class="shortcut">{{(index .vars.BayMappings .id).Shortcut}}</div>
|
||||
<div class="hatch-panel"></div>
|
||||
<div class="cargo"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -8,14 +8,49 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Team254/cheesy-arena/field"
|
||||
"github.com/Team254/cheesy-arena/game"
|
||||
"github.com/Team254/cheesy-arena/model"
|
||||
"github.com/Team254/cheesy-arena/websocket"
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Maps a numbered bay on the scoring panel to the field that it represents in the Score model.
|
||||
type bayMapping struct {
|
||||
BayId int
|
||||
Shortcut string
|
||||
RedElement string
|
||||
RedIndex int
|
||||
BlueElement string
|
||||
BlueIndex int
|
||||
}
|
||||
|
||||
var bayMappings = []*bayMapping{
|
||||
{0, "q", "rocketNearRight", 2, "rocketFarRight", 2},
|
||||
{1, "a", "rocketNearRight", 1, "rocketFarRight", 1},
|
||||
{2, "z", "rocketNearRight", 0, "rocketFarRight", 0},
|
||||
{3, "w", "rocketNearLeft", 2, "rocketFarLeft", 2},
|
||||
{4, "s", "rocketNearLeft", 1, "rocketFarLeft", 1},
|
||||
{5, "x", "rocketNearLeft", 0, "rocketFarLeft", 0},
|
||||
{6, "e", "cargoShip", 0, "cargoShip", 7},
|
||||
{7, "d", "cargoShip", 1, "cargoShip", 6},
|
||||
{8, "c", "cargoShip", 2, "cargoShip", 5},
|
||||
{9, "v", "cargoShip", 3, "cargoShip", 4},
|
||||
{10, "b", "cargoShip", 4, "cargoShip", 3},
|
||||
{11, "n", "cargoShip", 5, "cargoShip", 2},
|
||||
{12, "j", "cargoShip", 6, "cargoShip", 1},
|
||||
{13, "i", "cargoShip", 7, "cargoShip", 0},
|
||||
{14, "o", "rocketFarRight", 2, "rocketNearRight", 2},
|
||||
{15, "k", "rocketFarRight", 1, "rocketNearRight", 1},
|
||||
{16, "m", "rocketFarRight", 0, "rocketNearRight", 0},
|
||||
{17, "p", "rocketFarLeft", 2, "rocketNearLeft", 2},
|
||||
{18, "l", "rocketFarLeft", 1, "rocketNearLeft", 1},
|
||||
{19, ",", "rocketFarLeft", 0, "rocketNearLeft", 0},
|
||||
}
|
||||
|
||||
// Renders the scoring interface which enables input of scores in real-time.
|
||||
func (web *Web) scoringPanelHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !web.userIsAdmin(w, r) {
|
||||
@@ -36,8 +71,9 @@ func (web *Web) scoringPanelHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
data := struct {
|
||||
*model.EventSettings
|
||||
Alliance string
|
||||
}{web.arena.EventSettings, alliance}
|
||||
Alliance string
|
||||
BayMappings []*bayMapping
|
||||
}{web.arena.EventSettings, alliance, bayMappings}
|
||||
err = template.ExecuteTemplate(w, "base_no_navbar", data)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
@@ -57,12 +93,13 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ
|
||||
handleWebErr(w, fmt.Errorf("Invalid alliance '%s'.", alliance))
|
||||
return
|
||||
}
|
||||
var score **field.RealtimeScore
|
||||
var realtimeScore **field.RealtimeScore
|
||||
if alliance == "red" {
|
||||
score = &web.arena.RedRealtimeScore
|
||||
realtimeScore = &web.arena.RedRealtimeScore
|
||||
} else {
|
||||
score = &web.arena.BlueRealtimeScore
|
||||
realtimeScore = &web.arena.BlueRealtimeScore
|
||||
}
|
||||
score := &(*realtimeScore).CurrentScore
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -72,12 +109,12 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client, in a separate goroutine.
|
||||
go ws.HandleNotifiers(web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
go ws.HandleNotifiers(web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.ReloadDisplaysNotifier)
|
||||
|
||||
// Loop, waiting for commands and responding to them, until the client closes the connection.
|
||||
for {
|
||||
messageType, _, err := ws.Read()
|
||||
command, _, err := ws.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
// Client has closed the connection; nothing to do here.
|
||||
@@ -88,39 +125,120 @@ func (web *Web) scoringPanelWebsocketHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
scoreChanged := false
|
||||
switch messageType {
|
||||
case "\r":
|
||||
if (web.arena.MatchState != field.PreMatch && web.arena.MatchState != field.TimeoutActive &&
|
||||
web.arena.MatchState != field.PostTimeout || web.arena.CurrentMatch.Type == "test") &&
|
||||
!(*score).AutoCommitted {
|
||||
(*score).AutoCommitted = true
|
||||
scoreChanged = true
|
||||
}
|
||||
case "a":
|
||||
if (*score).AutoCommitted {
|
||||
(*score).AutoCommitted = false
|
||||
scoreChanged = true
|
||||
}
|
||||
case "commitMatch":
|
||||
|
||||
if command == "commitMatch" {
|
||||
if web.arena.MatchState != field.PostMatch {
|
||||
// Don't allow committing the score until the match is over.
|
||||
ws.WriteError("Cannot commit score: Match is not over.")
|
||||
continue
|
||||
}
|
||||
|
||||
if !(*score).TeleopCommitted {
|
||||
(*score).AutoCommitted = true
|
||||
(*score).TeleopCommitted = true
|
||||
if !(*realtimeScore).TeleopCommitted {
|
||||
(*realtimeScore).TeleopCommitted = true
|
||||
web.arena.ScoringStatusNotifier.Notify()
|
||||
scoreChanged = true
|
||||
}
|
||||
default:
|
||||
// Unknown keypress; just swallow the message without doing anything.
|
||||
continue
|
||||
} else if number, err := strconv.Atoi(command); err == nil && number >= 1 && number <= 9 {
|
||||
// Handle per-robot scoring fields.
|
||||
if number <= 3 {
|
||||
index := number - 1
|
||||
score.RobotStartLevels[index]++
|
||||
if score.RobotStartLevels[index] == 4 {
|
||||
score.RobotStartLevels[index] = 0
|
||||
}
|
||||
scoreChanged = true
|
||||
} else if number <= 6 && web.arena.MatchState != field.PreMatch {
|
||||
index := number - 4
|
||||
score.SandstormBonuses[index] =
|
||||
!score.SandstormBonuses[index]
|
||||
scoreChanged = true
|
||||
} else if web.arena.MatchState != field.PreMatch {
|
||||
index := number - 7
|
||||
score.RobotEndLevels[index]++
|
||||
if score.RobotEndLevels[index] == 4 {
|
||||
score.RobotEndLevels[index] = 0
|
||||
}
|
||||
scoreChanged = true
|
||||
}
|
||||
} else {
|
||||
// Handle cargo bays.
|
||||
var bayMapping *bayMapping
|
||||
for _, mapping := range bayMappings {
|
||||
if mapping.Shortcut == command {
|
||||
bayMapping = mapping
|
||||
break
|
||||
}
|
||||
}
|
||||
if bayMapping != nil {
|
||||
element := bayMapping.RedElement
|
||||
index := bayMapping.RedIndex
|
||||
if alliance == "blue" {
|
||||
element = bayMapping.BlueElement
|
||||
index = bayMapping.BlueIndex
|
||||
}
|
||||
switch element {
|
||||
case "cargoShip":
|
||||
scoreChanged = web.toggleCargoShipBay(&score.CargoBays[index], index)
|
||||
case "rocketNearLeft":
|
||||
scoreChanged = web.toggleRocketBay(&score.RocketNearLeftBays[index])
|
||||
case "rocketNearRight":
|
||||
scoreChanged = web.toggleRocketBay(&score.RocketNearRightBays[index])
|
||||
case "rocketFarLeft":
|
||||
scoreChanged = web.toggleRocketBay(&score.RocketFarLeftBays[index])
|
||||
case "rocketFarRight":
|
||||
scoreChanged = web.toggleRocketBay(&score.RocketFarRightBays[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if scoreChanged {
|
||||
if web.arena.MatchState == field.PreMatch {
|
||||
score.CargoBaysPreMatch = score.CargoBays
|
||||
}
|
||||
web.arena.RealtimeScoreNotifier.Notify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advances the given cargo ship bay through the states applicable to the current status of the field.
|
||||
func (web *Web) toggleCargoShipBay(bay *game.BayStatus, index int) bool {
|
||||
if (index == 3 || index == 4) && web.arena.MatchState == field.PreMatch {
|
||||
// Only the side bays can be preloaded.
|
||||
return false
|
||||
}
|
||||
|
||||
if web.arena.MatchState == field.PreMatch {
|
||||
*bay++
|
||||
if *bay == game.BayHatchCargo {
|
||||
// Skip the hatch+cargo state pre-match as it is invalid.
|
||||
*bay = game.BayCargo
|
||||
} else if *bay > game.BayCargo {
|
||||
*bay = game.BayEmpty
|
||||
}
|
||||
} else {
|
||||
if *bay == game.BayCargo {
|
||||
// If the bay was pre-loaded with cargo, go immediately to hatch+cargo during first toggle.
|
||||
*bay = game.BayHatchCargo
|
||||
} else {
|
||||
*bay++
|
||||
if *bay == game.BayCargo {
|
||||
// Skip the cargo-only state during the match as it can't stay in on its own.
|
||||
*bay = game.BayEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Advances the given rocket bay through the states applicable to the current status of the field.
|
||||
func (web *Web) toggleRocketBay(bay *game.BayStatus) bool {
|
||||
if web.arena.MatchState != field.PreMatch {
|
||||
*bay++
|
||||
if *bay == game.BayCargo {
|
||||
// Skip the cargo-only state as it's not applicable to rocket bays.
|
||||
*bay = game.BayEmpty
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -42,8 +42,10 @@ func TestScoringPanelWebsocket(t *testing.T) {
|
||||
blueWs := websocket.NewTestWebsocket(blueConn)
|
||||
|
||||
// Should receive a score update right after connection.
|
||||
readWebsocketType(t, redWs, "matchLoad")
|
||||
readWebsocketType(t, redWs, "matchTime")
|
||||
readWebsocketType(t, redWs, "realtimeScore")
|
||||
readWebsocketType(t, blueWs, "matchLoad")
|
||||
readWebsocketType(t, blueWs, "matchTime")
|
||||
readWebsocketType(t, blueWs, "realtimeScore")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user