From 8af83ea7e7ce33f4cf250bf1518eb0af0cd3c28c Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 7 Aug 2016 14:15:57 -0700 Subject: [PATCH] Updated realtime scoring entry for 2016. --- arena.go | 1 - scoring_display.go | 116 ++++++++++++++++++-- scoring_display_test.go | 190 ++++++++++++++++++--------------- static/css/cheesy-arena.css | 46 -------- static/js/scoring_display.js | 174 +++++++++++------------------- templates/scoring_display.html | 146 ++++++++++++++----------- 6 files changed, 356 insertions(+), 317 deletions(-) diff --git a/arena.go b/arena.go index 6acba0c..6abaa1d 100644 --- a/arena.go +++ b/arena.go @@ -50,7 +50,6 @@ type RealtimeScore struct { AutoCommitted bool TeleopCommitted bool FoulsCommitted bool - undoScores []Score } type Arena struct { diff --git a/scoring_display.go b/scoring_display.go index a78af72..b623267 100644 --- a/scoring_display.go +++ b/scoring_display.go @@ -11,6 +11,7 @@ import ( "io" "log" "net/http" + "strconv" "text/template" ) @@ -123,7 +124,7 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { // Loop, waiting for commands and responding to them, until the client closes the connection. for { - messageType, _, err := websocket.Read() + messageType, data, err := websocket.Read() if err != nil { if err == io.EOF { // Client has closed the connection; nothing to do here. @@ -134,7 +135,113 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { } switch messageType { - // TODO(patrick): Add 2016 messages. + case "defenseCrossed": + position, ok := data.(string) + if !ok { + websocket.WriteError("Defense position is not a string.") + continue + } + intPosition, err := strconv.Atoi(position) + if err != nil { + websocket.WriteError(err.Error()) + continue + } + if (*score).CurrentScore.AutoDefensesCrossed[intPosition-1]+ + (*score).CurrentScore.DefensesCrossed[intPosition-1] < 2 { + if !(*score).AutoCommitted { + (*score).CurrentScore.AutoDefensesCrossed[intPosition-1]++ + } else { + (*score).CurrentScore.DefensesCrossed[intPosition-1]++ + } + } + case "undoDefenseCrossed": + position, ok := data.(string) + if !ok { + websocket.WriteError("Defense position is not a string.") + continue + } + intPosition, err := strconv.Atoi(position) + if err != nil { + websocket.WriteError(err.Error()) + continue + } + if !(*score).AutoCommitted { + if (*score).CurrentScore.AutoDefensesCrossed[intPosition-1] > 0 { + (*score).CurrentScore.AutoDefensesCrossed[intPosition-1]-- + } + } else { + if (*score).CurrentScore.DefensesCrossed[intPosition-1] > 0 { + (*score).CurrentScore.DefensesCrossed[intPosition-1]-- + } + } + case "autoDefenseReached": + if !(*score).AutoCommitted { + if (*score).CurrentScore.AutoDefensesReached < 3 { + (*score).CurrentScore.AutoDefensesReached++ + } + } + case "undoAutoDefenseReached": + if !(*score).AutoCommitted { + if (*score).CurrentScore.AutoDefensesReached > 0 { + (*score).CurrentScore.AutoDefensesReached-- + } + } + case "highGoal": + if !(*score).AutoCommitted { + (*score).CurrentScore.AutoHighGoals++ + } else { + (*score).CurrentScore.HighGoals++ + } + case "undoHighGoal": + if !(*score).AutoCommitted { + if (*score).CurrentScore.AutoHighGoals > 0 { + (*score).CurrentScore.AutoHighGoals-- + } + } else { + if (*score).CurrentScore.HighGoals > 0 { + (*score).CurrentScore.HighGoals-- + } + } + case "lowGoal": + if !(*score).AutoCommitted { + (*score).CurrentScore.AutoLowGoals++ + } else { + (*score).CurrentScore.LowGoals++ + } + case "undoLowGoal": + if !(*score).AutoCommitted { + if (*score).CurrentScore.AutoLowGoals > 0 { + (*score).CurrentScore.AutoLowGoals-- + } + } else { + if (*score).CurrentScore.LowGoals > 0 { + (*score).CurrentScore.LowGoals-- + } + } + case "challenge": + if (*score).AutoCommitted { + if (*score).CurrentScore.Challenges < 3 { + (*score).CurrentScore.Challenges++ + } + } + case "undoChallenge": + if (*score).AutoCommitted { + if (*score).CurrentScore.Challenges > 0 { + (*score).CurrentScore.Challenges-- + } + } + case "scale": + if (*score).AutoCommitted { + if (*score).CurrentScore.Scales < 3 { + (*score).CurrentScore.Scales++ + } + } + case "undoScale": + if (*score).AutoCommitted { + if (*score).CurrentScore.Scales > 0 { + (*score).CurrentScore.Scales-- + } + } case "commit": (*score).AutoCommitted = true case "uncommitAuto": @@ -149,11 +256,6 @@ func ScoringDisplayWebsocketHandler(w http.ResponseWriter, r *http.Request) { (*score).AutoCommitted = true (*score).TeleopCommitted = true mainArena.scoringStatusNotifier.Notify(nil) - case "undo": - if len((*score).undoScores) > 0 { - (*score).CurrentScore = (*score).undoScores[len((*score).undoScores)-1] - (*score).undoScores = (*score).undoScores[0 : len((*score).undoScores)-1] - } default: websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType)) continue diff --git a/scoring_display_test.go b/scoring_display_test.go index 78f0746..4f3f45d 100644 --- a/scoring_display_test.go +++ b/scoring_display_test.go @@ -4,6 +4,7 @@ package main import ( + "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "testing" ) @@ -29,101 +30,114 @@ func TestScoringDisplay(t *testing.T) { } func TestScoringDisplayWebsocket(t *testing.T) { - // TODO(patrick): Update for 2016. - /* - clearDb() - defer clearDb() - var err error - db, err = OpenDatabase(testDbPath) - assert.Nil(t, err) - defer db.Close() - eventSettings, _ = db.GetEventSettings() - mainArena.Setup() + clearDb() + defer clearDb() + var err error + db, err = OpenDatabase(testDbPath) + assert.Nil(t, err) + defer db.Close() + eventSettings, _ = db.GetEventSettings() + mainArena.Setup() - server, wsUrl := startTestServer() - defer server.Close() - _, _, err = websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil) - assert.NotNil(t, err) - redConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/red/websocket", nil) - assert.Nil(t, err) - defer redConn.Close() - redWs := &Websocket{redConn} - blueConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blue/websocket", nil) - assert.Nil(t, err) - defer blueConn.Close() - blueWs := &Websocket{blueConn} + server, wsUrl := startTestServer() + defer server.Close() + _, _, err = websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blorpy/websocket", nil) + assert.NotNil(t, err) + redConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/red/websocket", nil) + assert.Nil(t, err) + defer redConn.Close() + redWs := &Websocket{redConn} + blueConn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/displays/scoring/blue/websocket", nil) + assert.Nil(t, err) + defer blueConn.Close() + blueWs := &Websocket{blueConn} - // Should receive a score update right after connection. + // Should receive a score update right after connection. + readWebsocketType(t, redWs, "score") + readWebsocketType(t, redWs, "matchTime") + readWebsocketType(t, blueWs, "score") + readWebsocketType(t, blueWs, "matchTime") + + // Send a match worth of scoring commands in. + redWs.Write("defenseCrossed", "2") + blueWs.Write("autoDefenseReached", nil) + redWs.Write("highGoal", nil) + redWs.Write("highGoal", nil) + redWs.Write("lowGoal", nil) + redWs.Write("defenseCrossed", "5") + blueWs.Write("defenseCrossed", "1") + redWs.Write("undoHighGoal", nil) + redWs.Write("commit", nil) + blueWs.Write("autoDefenseReached", nil) + blueWs.Write("commit", nil) + redWs.Write("uncommitAuto", nil) + redWs.Write("autoDefenseReached", nil) + redWs.Write("defenseCrossed", "2") + redWs.Write("commit", nil) + for i := 0; i < 11; i++ { readWebsocketType(t, redWs, "score") - readWebsocketType(t, redWs, "matchTime") + } + for i := 0; i < 4; i++ { readWebsocketType(t, blueWs, "score") - readWebsocketType(t, blueWs, "matchTime") + } - // Send a match worth of scoring commands in. - redWs.Write("robotSet", nil) - blueWs.Write("containerSet", nil) - redWs.Write("stackedToteSet", nil) - redWs.Write("robotSet", nil) - redWs.Write("toteSet", nil) - blueWs.Write("stackedToteSet", nil) - redWs.Write("commit", nil) - blueWs.Write("commit", nil) - redWs.Write("uncommitAuto", nil) - redWs.Write("robotSet", nil) - redWs.Write("commit", nil) - for i := 0; i < 8; i++ { - readWebsocketType(t, redWs, "score") - } - for i := 0; i < 3; i++ { - readWebsocketType(t, blueWs, "score") - } + assert.Equal(t, [5]int{0, 2, 0, 0, 1}, mainArena.redRealtimeScore.CurrentScore.AutoDefensesCrossed) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoDefensesReached) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoHighGoals) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoLowGoals) + assert.Equal(t, [5]int{1, 0, 0, 0, 0}, mainArena.blueRealtimeScore.CurrentScore.AutoDefensesCrossed) + assert.Equal(t, 2, mainArena.blueRealtimeScore.CurrentScore.AutoDefensesReached) - assert.True(t, mainArena.redRealtimeScore.CurrentScore.AutoRobotSet) - assert.False(t, mainArena.redRealtimeScore.CurrentScore.AutoContainerSet) - assert.True(t, mainArena.redRealtimeScore.CurrentScore.AutoToteSet) - assert.False(t, mainArena.redRealtimeScore.CurrentScore.AutoStackedToteSet) - assert.False(t, mainArena.blueRealtimeScore.CurrentScore.AutoRobotSet) - assert.True(t, mainArena.blueRealtimeScore.CurrentScore.AutoContainerSet) - assert.False(t, mainArena.blueRealtimeScore.CurrentScore.AutoToteSet) - assert.True(t, mainArena.blueRealtimeScore.CurrentScore.AutoStackedToteSet) - - stacks := []Stack{Stack{6, true, true}, Stack{1, false, false}, Stack{2, true, false}, Stack{}} - blueWs.Write("commit", stacks) - redWs.Write("commit", stacks) - stacks[0].Litter = false - blueWs.Write("commit", stacks) - redWs.Write("toteSet", nil) - blueWs.Write("stackedToteSet", nil) - for i := 0; i < 2; i++ { - readWebsocketType(t, redWs, "score") - } - for i := 0; i < 3; i++ { - readWebsocketType(t, blueWs, "score") - } - assert.Equal(t, stacks, mainArena.blueRealtimeScore.CurrentScore.Stacks) - stacks[0].Litter = true - assert.Equal(t, stacks, mainArena.redRealtimeScore.CurrentScore.Stacks) - - // Test committing logic. - redWs.Write("commitMatch", nil) - readWebsocketType(t, redWs, "error") - mainArena.MatchState = POST_MATCH - redWs.Write("commitMatch", nil) - blueWs.Write("commitMatch", nil) - readWebsocketType(t, redWs, "dialog") // Should be an error message about co-op not matching. - readWebsocketType(t, blueWs, "dialog") - redWs.Write("stackedToteSet", nil) - redWs.Write("commitMatch", nil) - blueWs.Write("commitMatch", nil) + redWs.Write("defenseCrossed", "2") + blueWs.Write("autoDefenseReached", nil) + redWs.Write("highGoal", nil) + redWs.Write("highGoal", nil) + redWs.Write("lowGoal", nil) + redWs.Write("defenseCrossed", "5") + blueWs.Write("defenseCrossed", "3") + blueWs.Write("challenge", nil) + blueWs.Write("scale", nil) + blueWs.Write("undoChallenge", nil) + redWs.Write("challenge", nil) + redWs.Write("defenseCrossed", "3") + redWs.Write("undoHighGoal", nil) + for i := 0; i < 8; i++ { readWebsocketType(t, redWs, "score") + } + for i := 0; i < 5; i++ { readWebsocketType(t, blueWs, "score") + } - // Load another match to reset the results. - mainArena.ResetMatch() - mainArena.LoadTestMatch() - readWebsocketType(t, redWs, "score") - readWebsocketType(t, blueWs, "score") - assert.Equal(t, *NewRealtimeScore(), *mainArena.redRealtimeScore) - assert.Equal(t, *NewRealtimeScore(), *mainArena.blueRealtimeScore) - */ + // Make sure auto scores haven't changed in teleop. + assert.Equal(t, [5]int{0, 2, 0, 0, 1}, mainArena.redRealtimeScore.CurrentScore.AutoDefensesCrossed) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoDefensesReached) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoHighGoals) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.AutoLowGoals) + assert.Equal(t, [5]int{1, 0, 0, 0, 0}, mainArena.blueRealtimeScore.CurrentScore.AutoDefensesCrossed) + assert.Equal(t, 2, mainArena.blueRealtimeScore.CurrentScore.AutoDefensesReached) + + assert.Equal(t, [5]int{0, 0, 1, 0, 1}, mainArena.redRealtimeScore.CurrentScore.DefensesCrossed) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.HighGoals) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.LowGoals) + assert.Equal(t, 1, mainArena.redRealtimeScore.CurrentScore.Challenges) + assert.Equal(t, [5]int{0, 0, 1, 0, 0}, mainArena.blueRealtimeScore.CurrentScore.DefensesCrossed) + assert.Equal(t, 0, mainArena.blueRealtimeScore.CurrentScore.Challenges) + assert.Equal(t, 1, mainArena.blueRealtimeScore.CurrentScore.Scales) + + // Test committing logic. + redWs.Write("commitMatch", nil) + readWebsocketType(t, redWs, "error") + mainArena.MatchState = POST_MATCH + redWs.Write("commitMatch", nil) + blueWs.Write("commitMatch", nil) + readWebsocketType(t, redWs, "score") + readWebsocketType(t, blueWs, "score") + + // Load another match to reset the results. + mainArena.ResetMatch() + mainArena.LoadTestMatch() + readWebsocketType(t, redWs, "score") + readWebsocketType(t, blueWs, "score") + assert.Equal(t, *NewRealtimeScore(), *mainArena.redRealtimeScore) + assert.Equal(t, *NewRealtimeScore(), *mainArena.blueRealtimeScore) } diff --git a/static/css/cheesy-arena.css b/static/css/cheesy-arena.css index b77e37d..3c7890e 100644 --- a/static/css/cheesy-arena.css +++ b/static/css/cheesy-arena.css @@ -99,55 +99,9 @@ .scoring-comment { font-size: 20px; } -.scoring-comment[data-value=true] { - color: #0a0; -} -.scoring-comment[data-value=false] { - color: #f00; -} .scoring-message { color: #f00; } -.stack-grid { - width: 90%; - margin: 5%; - border: 2px solid transparent; -} -.stack-grid[data-changed=true] { - border: 2px solid #fc0; -} -.stack-grid td { - width: 20%; - height: 120px; -} -.stack-grid td[data-selected=true] { - background-color: #ffc; -} -.stack-tote-count { - color: #999; - font-size: 40px; - padding: 20px; -} -.stack-container { - position: relative; - left: 47px; - bottom: 50px; - margin-bottom: -40px; - border-top: 40px solid #696; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - width: 35px; - height: 0; -} -.stack-litter { - position: relative; - left: 60px; - bottom: 80px; - margin-bottom: -30px; - width: 10px; - height: 30px; - background-color: #9f0; -} .btn-lower-third { width: 80px; } diff --git a/static/js/scoring_display.js b/static/js/scoring_display.js index 37986fb..abfbfd2 100644 --- a/static/js/scoring_display.js +++ b/static/js/scoring_display.js @@ -4,55 +4,31 @@ // Client-side logic for the scoring interface. var websocket; -var selectedStack = 0; -var numStacks = 10; -var stacks; -var stackScoreChanged = false; var scoreCommitted = false; -function Stack() { - this.Totes = 0; - this.Container = false; - this.Litter = false; -} - // Handles a websocket message to update the realtime scoring fields. var handleScore = function(data) { // Update autonomous period values. var score = data.CurrentScore; - $("#autoRobotSet").text(score.AutoRobotSet ? "Yes" : "No"); - $("#autoRobotSet").attr("data-value", score.AutoRobotSet); - $("#autoContainerSet").text(score.AutoContainerSet ? "Yes" : "No"); - $("#autoContainerSet").attr("data-value", score.AutoContainerSet); - $("#autoToteSet").text(score.AutoToteSet ? "Yes" : "No"); - $("#autoToteSet").attr("data-value", score.AutoToteSet); - $("#autoStackedToteSet").text(score.AutoStackedToteSet ? "Yes" : "No"); - $("#autoStackedToteSet").attr("data-value", score.AutoStackedToteSet); + $("#autoDefense1Crossings").text(score.AutoDefensesCrossed[0]); + $("#autoDefense2Crossings").text(score.AutoDefensesCrossed[1]); + $("#autoDefense3Crossings").text(score.AutoDefensesCrossed[2]); + $("#autoDefense4Crossings").text(score.AutoDefensesCrossed[3]); + $("#autoDefense5Crossings").text(score.AutoDefensesCrossed[4]); + $("#autoDefensesReached").text(score.AutoDefensesReached); + $("#autoHighGoals").text(score.AutoHighGoals); + $("#autoLowGoals").text(score.AutoLowGoals); // Update teleoperated period values. - $("#coopertitionSet").text(score.CoopertitionSet ? "Yes" : "No"); - $("#coopertitionSet").attr("data-value", score.CoopertitionSet); - $("#coopertitionStack").text(score.CoopertitionStack ? "Yes" : "No"); - $("#coopertitionStack").attr("data-value", score.CoopertitionStack); - - // Don't stomp on pending changes to the stack score. - if (stackScoreChanged == false) { - if (score.Stacks == null) { - stacks = new Array(); - for (i = 0; i < numStacks; i++) { - stacks.push(new Stack()); - } - } else { - stacks = score.Stacks; - } - for (i = 0; i < numStacks; i++) { - updateStackView(i); - } - - // Reset indications that the stack score is uncommitted. - $("#teleopMessage").css("opacity", 0); - $(".stack-grid").attr("data-changed", false); - } + $("#defense1Crossings").text(score.DefensesCrossed[0] + " (" + score.AutoDefensesCrossed[0] + " in auto)"); + $("#defense2Crossings").text(score.DefensesCrossed[1] + " (" + score.AutoDefensesCrossed[1] + " in auto)"); + $("#defense3Crossings").text(score.DefensesCrossed[2] + " (" + score.AutoDefensesCrossed[2] + " in auto)"); + $("#defense4Crossings").text(score.DefensesCrossed[3] + " (" + score.AutoDefensesCrossed[3] + " in auto)"); + $("#defense5Crossings").text(score.DefensesCrossed[4] + " (" + score.AutoDefensesCrossed[4] + " in auto)"); + $("#highGoals").text(score.HighGoals); + $("#lowGoals").text(score.LowGoals); + $("#challenges").text(score.Challenges); + $("#scales").text(score.Scales); // Update component visibility. if (!data.AutoCommitted) { @@ -83,64 +59,61 @@ var handleScore = function(data) { // Handles a keyboard event and sends the appropriate websocket message. var handleKeyPress = function(event) { var key = String.fromCharCode(event.keyCode); - switch(key) { + switch (key) { + case "1": + case "2": + case "3": + case "4": + case "5": + websocket.send("defenseCrossed", key); + break; + case "!": + websocket.send("undoDefenseCrossed", "1"); + break; + case "@": + websocket.send("undoDefenseCrossed", "2"); + break; + case "#": + websocket.send("undoDefenseCrossed", "3"); + break; + case "$": + websocket.send("undoDefenseCrossed", "4"); + break; + case "%": + websocket.send("undoDefenseCrossed", "5"); + break; case "r": - websocket.send("robotSet"); + websocket.send("autoDefenseReached"); break; - case "c": - if ($("#autoCommands").is(":visible")) { - websocket.send("containerSet"); - } else { - stacks[selectedStack].Container = !stacks[selectedStack].Container; - if (!stacks[selectedStack].Container) { - stacks[selectedStack].Litter = false; - } - updateStackView(selectedStack); - invalidateStackScore(); - } + case "R": + websocket.send("undoAutoDefenseReached"); break; - case "t": - websocket.send("toteSet"); + case "h": + websocket.send("highGoal"); break; - case "s": - websocket.send("stackedToteSet"); - break; - case "j": - if (selectedStack > 0) { - selectedStack--; - updateSelectedStack(); - } + case "H": + websocket.send("undoHighGoal"); break; case "l": - if (selectedStack < numStacks - 1) { - selectedStack++; - updateSelectedStack(); - } + websocket.send("lowGoal"); break; - case "i": - if (stacks[selectedStack].Totes < 6) { - stacks[selectedStack].Totes++; - updateStackView(selectedStack); - invalidateStackScore(); - } + case "L": + websocket.send("undoLowGoal"); break; - case "k": - if (stacks[selectedStack].Totes > 0) { - stacks[selectedStack].Totes--; - updateStackView(selectedStack); - invalidateStackScore(); - } + case "c": + websocket.send("challenge"); break; - case "n": - if (stacks[selectedStack].Container) { - stacks[selectedStack].Litter = !stacks[selectedStack].Litter; - updateStackView(selectedStack); - invalidateStackScore(); - } + case "C": + websocket.send("undoChallenge"); + break; + case "s": + websocket.send("scale"); + break; + case "S": + websocket.send("undoScale"); break; case "\r": - websocket.send("commit", stacks); - stackScoreChanged = false; + websocket.send("commit"); break; case "a": websocket.send("uncommitAuto"); @@ -157,31 +130,8 @@ var handleMatchTime = function(data) { } }; -// Updates the stack grid to highlight only the active stack. -var updateSelectedStack = function() { - for (i = 0; i < numStacks; i++) { - $("#stack" + i).attr("data-selected", i == selectedStack); - } -}; - -// Updates the appearance of the given stack in the grid to match the scoring data. -var updateStackView = function(stackIndex) { - stack = stacks[stackIndex]; - $("#stack" + stackIndex + " .stack-tote-count").text(stack.Totes); - $("#stack" + stackIndex + " .stack-container").toggle(stack.Container); - $("#stack" + stackIndex + " .stack-litter").toggle(stack.Litter); -}; - -// Shows message indicating that the stack score has been changed but not yet sent to the server. -var invalidateStackScore = function() { - $("#teleopMessage").css("opacity", 1); - $(".stack-grid").attr("data-changed", true); - stackScoreChanged = true; -}; - // Sends a websocket message to indicate that the score for this alliance is ready. var commitMatchScore = function() { - websocket.send("commit", stacks); websocket.send("commitMatch"); }; @@ -192,7 +142,5 @@ $(function() { matchTime: function(event) { handleMatchTime(event.data); } }); - updateSelectedStack(); - $(document).keypress(handleKeyPress); }); diff --git a/templates/scoring_display.html b/templates/scoring_display.html index 0d61872..e35f213 100644 --- a/templates/scoring_display.html +++ b/templates/scoring_display.html @@ -16,20 +16,24 @@

Autonomous Period

Use the following keyboard shortcuts:

-
r
-
Toggle robot set
+
1-5
+
Defense crossed +
-
c
-
Toggle container set
+
Shift+1-5
+
Defense crossed -
-
t
-
Toggle tote set
+
r/R
+
Defenses reached +/-
-
s
-
Toggle stacked tote set
+
h/H
+
High goals +/-
+
+
+
l/L
+
Low goals +/-
Enter
@@ -40,32 +44,28 @@

Teleoperated Period

Use the following keyboard shortcuts:

-
t
-
Toggle co-op tote set
+
1-5
+
Defense crossed +
-
s
-
Toggle co-op stacked tote set
+
Shift+1-5
+
Defense crossed -
-
j/l
-
Change selected stack
+
h/H
+
High goals +/-
-
i/k
-
Add/remove totes for selected stack
+
l/L
+
Low goals +/-
-
c
-
Toggle container for selected stack
+
c/C
+
Challenges +/-
-
n
-
Toggle noodle for selected stack
-
-
-
Enter
-
Commit tote stack change
+
s/S
+
Scales +/-
a
@@ -77,54 +77,83 @@
@@ -141,10 +170,3 @@ {{end}} -{{define "stack"}} - -
0
- - - -{{end}}