Merge pull request #29 from ZeusWPI/design/controller
Start working on redesign of controller page
This commit is contained in:
commit
0c1722a330
11 changed files with 479 additions and 255 deletions
|
@ -1,34 +0,0 @@
|
||||||
body {
|
|
||||||
background: #222;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#display {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#modules div {
|
|
||||||
display: inline-block;
|
|
||||||
color: #111;
|
|
||||||
font-size: 14pt;
|
|
||||||
font-family: monospace;
|
|
||||||
border-radius: 25%;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solved {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.needy {
|
|
||||||
background-color: grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsolved {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
|
@ -1,23 +1,48 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<title>OBUS controller</title>
|
<link rel="stylesheet" href="/static/css/controller.css" />
|
||||||
<link rel="stylesheet" href="/static/controller.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<title>OBUS controller</title>
|
||||||
<p>Game state: <span id="gamestate"></span></p>
|
</head>
|
||||||
|
|
||||||
<button onclick="startbutton()">START</button>
|
<body>
|
||||||
<button onclick="restartbutton()">RESTART</button>
|
<!-- Hud -->
|
||||||
<script src="static/controller.js"></script>
|
<div class="hud">
|
||||||
|
<!-- State -->
|
||||||
|
<div>
|
||||||
|
<div>Game state: <span id="gamestate"></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="center">
|
<!-- Actions -->
|
||||||
<canvas id="display" width="1000" height="500">Timer should be here</canvas>
|
<div>
|
||||||
</div>
|
<button id="buttonStart" onclick="onStartButtonClick()">START</button>
|
||||||
<script type="text/javascript" src="/static/segment-display.js"></script>
|
<button id="buttonRestart" onclick="onRestartButtonClick()">
|
||||||
<div id="modules" class="center"></div>
|
RESTART
|
||||||
</body>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="content">
|
||||||
|
<!-- Timer -->
|
||||||
|
<div class="box center">
|
||||||
|
<canvas id="display" width="1000" height="400">
|
||||||
|
Timer should be here
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modules -->
|
||||||
|
<div class="box center" style="margin-top: 2rem">
|
||||||
|
<div class="box-header">Modules</div>
|
||||||
|
|
||||||
|
<div id="modules"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="/static/js/controller.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/segment-display.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
let estimated_timeout_date;
|
|
||||||
let display;
|
|
||||||
let gamestate;
|
|
||||||
let last_strike_amount = 0;
|
|
||||||
let strike_audio = new Audio('/static/strike.mp3');
|
|
||||||
strike_audio.preload = 'auto';
|
|
||||||
let last_puzzle_state = []
|
|
||||||
|
|
||||||
function startbutton() {
|
|
||||||
fetch('/start');
|
|
||||||
}
|
|
||||||
|
|
||||||
function restartbutton() {
|
|
||||||
fetch('/restart');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDisplay() {
|
|
||||||
if (gamestate != 'GAME' || !estimated_timeout_date) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeleft((estimated_timeout_date - (new Date()))/1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateModuleState(data) {
|
|
||||||
if (data.gamestate == 'INACTIVE' || data.gamestate == 'INFO') {
|
|
||||||
document.getElementById("modules").innerHTML = '';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let newmodulestate = [];
|
|
||||||
for (const puzzlestate of data.puzzles) {
|
|
||||||
let modulediv = document.createElement('div');
|
|
||||||
modulediv.textContent = puzzlestate.address;
|
|
||||||
if (puzzlestate.solved === true) {
|
|
||||||
modulediv.className = "solved";
|
|
||||||
} else if (puzzlestate.solved === false) {
|
|
||||||
modulediv.className = "unsolved";
|
|
||||||
} else if (puzzlestate.solved === null) {
|
|
||||||
modulediv.className = "needy";
|
|
||||||
}
|
|
||||||
newmodulestate.push(modulediv);
|
|
||||||
}
|
|
||||||
document.getElementById("modules").replaceChildren(...newmodulestate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTimeleft(timeleft) {
|
|
||||||
if (timeleft <= 0) {
|
|
||||||
display.setValue('00:00.0');
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let integral = Math.floor(timeleft);
|
|
||||||
let fractional = timeleft - integral;
|
|
||||||
let minutes = Math.floor(integral / 60);
|
|
||||||
let seconds = integral % 60;
|
|
||||||
display.setValue(String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + '.' + String(Math.floor(fractional * 10)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function state_update() {
|
|
||||||
// TODO automatically timeout this request after the update interval
|
|
||||||
fetch('/status.json')
|
|
||||||
.then(
|
|
||||||
function(response) {
|
|
||||||
if (response.status !== 200) {
|
|
||||||
console.log('FAIL: ' + response.status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response.json().then(function(data) {
|
|
||||||
gamestate = data.gamestate;
|
|
||||||
document.getElementById('gamestate').innerHTML = gamestate;
|
|
||||||
if (gamestate != 'GAME') {
|
|
||||||
last_strike_amount = 0;
|
|
||||||
}
|
|
||||||
else if (gamestate == 'GAME') {
|
|
||||||
// TODO maybe smooth this with the previous value of estimated_timeout_date?
|
|
||||||
let new_estimate = new Date();
|
|
||||||
new_estimate.setTime(new_estimate.getTime() + data.timeleft*1000);
|
|
||||||
estimated_timeout_date = new_estimate;
|
|
||||||
let total_strikes = data.puzzles.map(x => x.strikes).reduce((a, b) => a + b, 0);
|
|
||||||
if (last_strike_amount < total_strikes) {
|
|
||||||
strike_audio.load();
|
|
||||||
strike_audio.play();
|
|
||||||
}
|
|
||||||
last_strike_amount = total_strikes;
|
|
||||||
}
|
|
||||||
updateModuleState(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = function() {
|
|
||||||
setInterval(state_update, 500);
|
|
||||||
display = new SegmentDisplay("display");
|
|
||||||
display.pattern = "##:##.#";
|
|
||||||
display.displayAngle = 0;
|
|
||||||
display.digitHeight = 100;
|
|
||||||
display.digitWidth = display.digitHeight / 2;
|
|
||||||
display.digitDistance = display.digitHeight / 10;
|
|
||||||
display.segmentWidth = display.digitHeight / 10;
|
|
||||||
display.segmentDistance = display.digitHeight / 100;
|
|
||||||
display.segmentCount = 7;
|
|
||||||
display.cornerType = 1;
|
|
||||||
display.colorOn = "#24dd22";
|
|
||||||
display.colorOff = "#1b4105";
|
|
||||||
display.draw();
|
|
||||||
display.draw();
|
|
||||||
setInterval(updateDisplay, 100);
|
|
||||||
};
|
|
108
python/static/css/controller.css
Normal file
108
python/static/css/controller.css
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "digital";
|
||||||
|
src: url("/static/fonts/digital-webfont.woff");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
font-family: "digital", sans-serif;
|
||||||
|
font-size: 32px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333;
|
||||||
|
background: #ddd;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 2px 2px #fff, inset -2px -2px #bbb,
|
||||||
|
4px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
color: #999999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttonStart {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-state {
|
||||||
|
font-size: #bbbbbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background: #000;
|
||||||
|
border: 5px solid #4d4d4d;
|
||||||
|
box-shadow: inset 0 0 0 10px #d6d6d6, 0 10px 0 -5px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-header {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display {
|
||||||
|
display: inline;
|
||||||
|
padding: 1rem 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modules > div {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333;
|
||||||
|
background: #ddd;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 3rem;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 2px 2px #fff, inset -2px -2px #bbb,
|
||||||
|
4px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solved {
|
||||||
|
background-color: green !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.needy {
|
||||||
|
background-color: grey !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsolved {
|
||||||
|
background-color: yellow !important;
|
||||||
|
}
|
|
@ -1,118 +1,140 @@
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<title>CAN debugger</title>
|
<title>CAN debugger</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.parsed {
|
.parsed {
|
||||||
background: lightgreen;
|
background: lightgreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.raw {
|
td.raw {
|
||||||
font-family: monospace, monospace;
|
font-family: monospace, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th,
|
||||||
border-left: 1px solid black;
|
td {
|
||||||
border-right: 1px solid black;
|
border-left: 1px solid black;
|
||||||
padding: 5px;
|
border-right: 1px solid black;
|
||||||
}
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: #ff7f00;;
|
background-color: #ff7f00;
|
||||||
border-bottom: 2px solid black;
|
border-bottom: 2px solid black;
|
||||||
border-top: 2px solid black;
|
border-top: 2px solid black;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fade {
|
.fade {
|
||||||
animation: fadein 0.5s;
|
animation: fadein 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.hide_raw .raw {
|
table.hide_raw .raw {
|
||||||
display:none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.hide_consecutive_states .staterow + .staterow {
|
table.hide_consecutive_states .staterow + .staterow {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:hover {
|
tr:hover {
|
||||||
background-color: #aaa;
|
background-color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.error, td.error > div {
|
td.error,
|
||||||
background-color: rgb(255, 71, 71);
|
td.error > div {
|
||||||
}
|
background-color: rgb(255, 71, 71);
|
||||||
|
}
|
||||||
|
|
||||||
td.controller > div {
|
td.controller > div {
|
||||||
background-color: lightseagreen;
|
background-color: lightseagreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.puzzle > div {
|
td.puzzle > div {
|
||||||
background-color: gold;
|
background-color: gold;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.needy > div {
|
td.needy > div {
|
||||||
background-color: rgb(128, 10, 128);
|
background-color: rgb(128, 10, 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
.time, .raw_id, .sender_id {
|
.time,
|
||||||
text-align: right;
|
.raw_id,
|
||||||
}
|
.sender_id {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.colorblock {
|
.colorblock {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
margin: 3.4px;
|
margin: 3.4px;
|
||||||
margin-right: 8.4px;
|
margin-right: 8.4px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color:lime;
|
background-color: lime;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<button onclick="toggle_logging()" id="toggle_button">Start</button>
|
<button onclick="toggle_logging()" id="toggle_button">Start</button>
|
||||||
<button onclick="clear_log()" id="clear_button">Clear</button>
|
<button onclick="clear_log()" id="clear_button">Clear</button>
|
||||||
|
|
||||||
<input type="checkbox" id="show_raw" name="show_raw" checked autocomplete="off" onchange="updateShowRaw()">
|
<input
|
||||||
<label for="show_raw">Show raw address and payload</label>
|
type="checkbox"
|
||||||
|
id="show_raw"
|
||||||
|
name="show_raw"
|
||||||
|
checked
|
||||||
|
autocomplete="off"
|
||||||
|
onchange="updateShowRaw()"
|
||||||
|
/>
|
||||||
|
<label for="show_raw">Show raw address and payload</label>
|
||||||
|
|
||||||
<input type="checkbox" id="show_consecutive_states" name="show_consecutive_states" checked autocomplete="off" onchange="updateShowStates()">
|
<input
|
||||||
<label for="show_consecutive_states">Show consecutive state updates</label>
|
type="checkbox"
|
||||||
|
id="show_consecutive_states"
|
||||||
|
name="show_consecutive_states"
|
||||||
|
checked
|
||||||
|
autocomplete="off"
|
||||||
|
onchange="updateShowStates()"
|
||||||
|
/>
|
||||||
|
<label for="show_consecutive_states">Show consecutive state updates</label>
|
||||||
|
|
||||||
<table id="message_table">
|
<table id="message_table">
|
||||||
<tr id="table_header">
|
<tr id="table_header">
|
||||||
<th>Human-readable type</th>
|
<th>Human-readable type</th>
|
||||||
<th>Sender ID</th>
|
<th>Sender ID</th>
|
||||||
<th>Parsed payload</th>
|
<th>Parsed payload</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
<th class="raw">Raw Message</th>
|
<th class="raw">Raw Message</th>
|
||||||
<th class="raw">Raw ID</th>
|
<th class="raw">Raw ID</th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<script src="static/debugscript.js"></script>
|
<script src="/static/js/debugscript.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
BIN
python/static/fonts/digital-webfont.woff
Normal file
BIN
python/static/fonts/digital-webfont.woff
Normal file
Binary file not shown.
211
python/static/js/controller.js
Normal file
211
python/static/js/controller.js
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/**
|
||||||
|
* Soundeffects
|
||||||
|
*/
|
||||||
|
const sounds = {
|
||||||
|
strike: new Audio("/static/sounds/strike.mp3"),
|
||||||
|
alarm: new Audio("/static/sounds/alarm.mp3"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State
|
||||||
|
*/
|
||||||
|
const state = {
|
||||||
|
// Segment display used for telling the remaining time.
|
||||||
|
display: null,
|
||||||
|
|
||||||
|
// Current game state.
|
||||||
|
gamestate: null,
|
||||||
|
|
||||||
|
// Estimated timeout date when the timer should be stopped.
|
||||||
|
estimatedTimeout: new Date(),
|
||||||
|
|
||||||
|
// Amount of strikes (wrong answers) the player has made.
|
||||||
|
strikes: 0,
|
||||||
|
|
||||||
|
// If the alarm has been played already
|
||||||
|
alarmPlayed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a given audio file
|
||||||
|
* @param {Audio} sound Audio object containing a sound.
|
||||||
|
*/
|
||||||
|
function playSound(sound) {
|
||||||
|
sound.load();
|
||||||
|
sound.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the modules.
|
||||||
|
* @param {Array} List of puzzles
|
||||||
|
*/
|
||||||
|
function updateModules(puzzles) {
|
||||||
|
// Delete the modules when the game is inactive or in info mode.
|
||||||
|
if (state.gamestate == "INACTIVE" || state.gamestate == "INFO") {
|
||||||
|
document.getElementById("modules").innerHTML = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modulesElement = document.getElementById("modules");
|
||||||
|
|
||||||
|
// Update the modules.
|
||||||
|
const modules = [];
|
||||||
|
for (const puzzle of puzzles) {
|
||||||
|
const moduleElement = document.createElement("div");
|
||||||
|
moduleElement.textContent = puzzle.address;
|
||||||
|
|
||||||
|
// State: Solved
|
||||||
|
if (puzzle.solved === true) {
|
||||||
|
moduleElement.className = "solved";
|
||||||
|
}
|
||||||
|
|
||||||
|
// State: Unsolved
|
||||||
|
if (puzzle.solved === false) {
|
||||||
|
moduleElement.className = "unsolved";
|
||||||
|
}
|
||||||
|
|
||||||
|
// State: Needy
|
||||||
|
if (puzzle.solved === null) {
|
||||||
|
moduleElement.className = "needy";
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.push(moduleElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
modulesElement.replaceChildren(...modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the game state.
|
||||||
|
*/
|
||||||
|
function initializeGameState() {
|
||||||
|
updateGameState();
|
||||||
|
setInterval(updateGameState, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the game state.
|
||||||
|
*/
|
||||||
|
function updateGameState() {
|
||||||
|
fetch("/status.json")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
// Update the game state
|
||||||
|
state.gamestate = data.gamestate;
|
||||||
|
document.getElementById("gamestate").innerHTML = state.gamestate;
|
||||||
|
|
||||||
|
// Reset the strike amount if the game is not running anymore.
|
||||||
|
if (state.gamestate != "GAME") {
|
||||||
|
state.strikeAmount = 0;
|
||||||
|
state.alarmPlayed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.gamestate === "GAME") {
|
||||||
|
// Update the estimated timeout date
|
||||||
|
state.estimatedTimeout = new Date(
|
||||||
|
new Date().getTime() + data.timeleft * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the total amount of strikes for all puzzles.
|
||||||
|
const newStrikes = data.puzzles
|
||||||
|
.map((p) => p.strikes)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
// Play a "buzzer" sound when a strike is made.
|
||||||
|
if (state.strikes < newStrikes) {
|
||||||
|
playSound(sounds.strike);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play a "alarm" sound when the time is at 10 seconds.
|
||||||
|
if (data.timeleft <= 20 && data.timeleft > 19 && !state.alarmPlayed) {
|
||||||
|
playSound(sounds.alarm);
|
||||||
|
state.alarmPlayed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the total amount of strikes
|
||||||
|
state.strikes = newStrikes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the start/restart button visibility.
|
||||||
|
const startButton = document.querySelector("#buttonStart");
|
||||||
|
const restartButton = document.querySelector("#buttonRestart");
|
||||||
|
startButton.disabled = state.gamestate !== "DISCOVER";
|
||||||
|
restartButton.disabled = state.gamestate !== "GAMEOVER";
|
||||||
|
|
||||||
|
// Update the modules
|
||||||
|
updateModules(data.puzzles);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the start button is clicked.
|
||||||
|
*/
|
||||||
|
function onStartButtonClick() {
|
||||||
|
fetch("/start");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the restart button is clicked.
|
||||||
|
*/
|
||||||
|
function onRestartButtonClick() {
|
||||||
|
fetch("/restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the segment display.
|
||||||
|
*/
|
||||||
|
function initializeSegmentDisplay() {
|
||||||
|
display = new SegmentDisplay("display");
|
||||||
|
display.pattern = "##:##.#";
|
||||||
|
display.displayAngle = 0;
|
||||||
|
display.digitHeight = 100;
|
||||||
|
display.digitWidth = display.digitHeight / 2;
|
||||||
|
display.digitDistance = display.digitHeight / 10;
|
||||||
|
display.segmentWidth = display.digitHeight / 10;
|
||||||
|
display.segmentDistance = display.digitHeight / 100;
|
||||||
|
display.segmentCount = 7;
|
||||||
|
display.cornerType = 1;
|
||||||
|
display.colorOn = "#ff0000";
|
||||||
|
display.colorOff = "#ff000022";
|
||||||
|
display.draw();
|
||||||
|
|
||||||
|
// Update the segment
|
||||||
|
setInterval(updateSegmentDisplay, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the segment display with the latest game data.
|
||||||
|
*/
|
||||||
|
function updateSegmentDisplay() {
|
||||||
|
// Do not update the timer when the game is not running.
|
||||||
|
if (state.gamestate != "GAME" || !state.estimatedTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLeft = (state.estimatedTimeout - new Date()) / 1000;
|
||||||
|
|
||||||
|
// Set the display to 0 when there is no time left.
|
||||||
|
if (timeLeft <= 0) {
|
||||||
|
display.setValue("00:00.0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integral = Math.floor(timeLeft);
|
||||||
|
const fractional = timeLeft - integral;
|
||||||
|
const minutes = Math.floor(integral / 60);
|
||||||
|
const seconds = integral % 60;
|
||||||
|
|
||||||
|
const minutesStr = String(minutes).padStart(2, "0");
|
||||||
|
const secondsStr = String(seconds).padStart(2, "0");
|
||||||
|
const fractionalStr = String(Math.floor(fractional * 10)).padStart(1, "0");
|
||||||
|
|
||||||
|
display.setValue(`${minutesStr}:${secondsStr}.${fractionalStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the window is loaded.
|
||||||
|
*/
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
initializeGameState();
|
||||||
|
initializeSegmentDisplay();
|
||||||
|
});
|
BIN
python/static/sounds/alarm.mp3
Normal file
BIN
python/static/sounds/alarm.mp3
Normal file
Binary file not shown.
Loading…
Reference in a new issue