mirror of https://github.com/dirtbags/moth.git
Merge 36e0ba1b8b
into 887e4b3eaf
This commit is contained in:
commit
b72f4061a0
|
@ -53,6 +53,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [v4.2.1] - 2021-04-13
|
||||
### Fixed
|
||||
- Transpiled KSAs no longer dropped
|
||||
- Resolve race condition to ensure Javascript dependencies load before "chart" is called
|
||||
- Add responsive design elements to Scoreboard to reduce object collision
|
||||
|
||||
|
||||
## [v4.2.0] - 2020-03-26
|
||||
### Changed
|
||||
|
|
|
@ -78,7 +78,34 @@ input:invalid {
|
|||
opacity: 0.8;
|
||||
position: absolute;
|
||||
right: 0.2em;
|
||||
background-color: #292929;
|
||||
background-blend-mode: darken;
|
||||
padding: 0em 0.2em;
|
||||
border-top-left-radius: 0.5em;
|
||||
border-bottom-left-radius: 0.5em;
|
||||
margin:0em;
|
||||
height: 1.5em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition-property: max-width;
|
||||
transition-duration: 2s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#rankings span.teamname:hover {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#rankings span.teampoints {
|
||||
font-size:100%;
|
||||
height:1.2em;
|
||||
margin:0em;
|
||||
padding:0em;
|
||||
width:99%;
|
||||
}
|
||||
|
||||
|
||||
#rankings div * {white-space: nowrap;}
|
||||
.cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;}
|
||||
.cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;}
|
||||
|
@ -90,6 +117,74 @@ input:invalid {
|
|||
.cat7, .cat15, .cat23 {background-color: #ff7f00; color: black;}
|
||||
|
||||
|
||||
/* Responsive design */
|
||||
/* Defaults */
|
||||
#rankings span.teampoints {
|
||||
max-width:89%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:10%;
|
||||
}
|
||||
|
||||
|
||||
/* Monitors with large enough screens to do side by side */
|
||||
@media only screen and (min-width: 170em) {
|
||||
#chart, #rankings {
|
||||
width: 49%;
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Monitor
|
||||
@media only screen and (max-width: 130em) {
|
||||
#chart, #rankings {
|
||||
width: 49%;
|
||||
display:inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#rankings span.teampoints {
|
||||
max-width:89%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:10%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Laptop size screen */
|
||||
@media only screen and (max-width: 100em) {
|
||||
#rankings span.teampoints {
|
||||
max-width:84%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:15%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Roughly Tablet size */
|
||||
@media only screen and (max-width: 70em) {
|
||||
#rankings span.teampoints {
|
||||
max-width:79%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:20%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small screens phone size */
|
||||
@media only screen and (max-width: 40em) {
|
||||
#rankings span.teampoints {
|
||||
max-width:65%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:34%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#devel {
|
||||
background-color: #eee;
|
||||
color: black;
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/* Moth ScreenSaver */
|
||||
#gibson {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Transparency is inverted from the clip. Mostly because it was easier this way, you can still make out the content behind it, and the black moths stick out better */
|
||||
#gibson.window {
|
||||
background-color: rgba(160, 160, 250, .2);
|
||||
|
||||
background-image:
|
||||
repeating-linear-gradient(to right, rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) 0.7em, rgba(0, 0, 0, 0) 1.3em, rgba(0, 0, 0, .8) 2em),
|
||||
repeating-linear-gradient(rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) .3em, rgba(0, 0, 0, 0) 3em, rgba(0, 0, 0, .8) 3.3em);
|
||||
}
|
||||
|
||||
#gibson.svg {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='5em' width='1.5em'><style>text { font: bold 1.5em Verdana, Helvetica, Arial, sans-serif; fill: rgba(255, 255, 255, 0.6); writing-mode: vertical-rl}</style><text x='0' y='0'>5928</text></svg>");
|
||||
|
||||
}
|
||||
|
||||
#gibson.combined {
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
|
||||
background-image:
|
||||
repeating-linear-gradient(to right, rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) 0.7em, rgba(0, 0, 0, 0) 1.3em, rgba(0, 0, 0, .8) 2em),
|
||||
repeating-linear-gradient(rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) .3em, rgba(0, 0, 0, 0) 3em, rgba(0, 0, 0, .8) 3.3em),
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='5em' width='1.5em'><style>text { font: bold 1.5em Verdana, Helvetica, Arial, sans-serif; fill: rgba(255, 255, 255, 0.6); writing-mode: vertical-rl}</style><text x='0' y='0'>MOTH</text></svg>");
|
||||
}
|
||||
|
||||
#gibson img.moth {
|
||||
top: 0;
|
||||
right: 100vw;
|
||||
position: absolute;
|
||||
width: 4em;
|
||||
height: auto;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
#gibson img.shadow, #gibson div.shadow {
|
||||
position: absolute;
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
#gibson div.shadow {
|
||||
background-color: #000;
|
||||
/* background-image: radial-gradient(rgba(0, 0, 0, 0), rgba(255, 255, 255, 0.9) 99%); */
|
||||
-webkit-mask-image: url("luna-moth.png");
|
||||
mask-image: url("luna-moth.png");
|
||||
-webkit-mask-size: 4em;
|
||||
mask-size: 4em;
|
||||
}
|
||||
|
||||
#gibson div.shadow.LR {
|
||||
transform: rotate(135deg) !important;
|
||||
background-color: #d00;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 170em) {
|
||||
#gibson img.moth, #gibson img.shadow {
|
||||
animation-duration: 12s;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 130em) {
|
||||
#gibson img.moth, #gibson img.shadow {
|
||||
animation-duration: 10s;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 100em) {
|
||||
#gibson img.moth, #gibson img.shadow {
|
||||
animation-duration: 8s;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 70em) {
|
||||
#gibson img.moth, #gibson img.shadow {
|
||||
animation-duration: 6s;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 40em) {
|
||||
#gibson img.moth, #gibson img.shadow {
|
||||
animation-duration: 4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideRL {
|
||||
from {
|
||||
left: 100vw;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
to {
|
||||
left: -5em;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideLR {
|
||||
from {
|
||||
left: -5em;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
to {
|
||||
left: 100vw;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
|
@ -0,0 +1,418 @@
|
|||
/* http://paletton.com/#uid=63T0u0k7O9o3ouT6LjHih7ltq4c */
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
max-width: 40em;
|
||||
background: #282a33;
|
||||
color: #f6efdc;
|
||||
}
|
||||
body.wide {
|
||||
max-width: 100%;
|
||||
}
|
||||
a:any-link {
|
||||
color: #8b969a;
|
||||
}
|
||||
h1 {
|
||||
background: #5e576b;
|
||||
color: #9e98a8;
|
||||
}
|
||||
.Fail, .Error, #messages {
|
||||
background: #3a3119;
|
||||
color: #ffcc98;
|
||||
}
|
||||
.Fail:before {
|
||||
content: "Fail: ";
|
||||
}
|
||||
.Error:before {
|
||||
content: "Error: ";
|
||||
}
|
||||
p {
|
||||
margin: 1em 0em;
|
||||
}
|
||||
form, pre {
|
||||
margin: 1em;
|
||||
}
|
||||
input, select {
|
||||
padding: 0.6em;
|
||||
margin: 0.2em;
|
||||
max-width: 30em;
|
||||
}
|
||||
nav {
|
||||
border: solid black 2px;
|
||||
}
|
||||
nav ul, .category ul {
|
||||
padding: 1em;
|
||||
}
|
||||
nav li, .category li {
|
||||
display: inline;
|
||||
margin: 1em;
|
||||
}
|
||||
iframe#body {
|
||||
border: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
input:invalid {
|
||||
border-color: red;
|
||||
}
|
||||
#messages {
|
||||
min-height: 3em;
|
||||
border: solid black 2px;
|
||||
}
|
||||
#rankings {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#rankings span {
|
||||
font-size: 75%;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
height: 1.7em;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
font-size: inherit;
|
||||
color: white;
|
||||
text-shadow: 0 0 3px black;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
right: 0.2em;
|
||||
background-color: #292929;
|
||||
background-blend-mode: darken;
|
||||
padding: 0em 0.2em;
|
||||
border-top-left-radius: 0.5em;
|
||||
border-bottom-left-radius: 0.5em;
|
||||
border-bottom: thin solid transparent;
|
||||
margin:0em;
|
||||
height: 1.5em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition-property: max-width;
|
||||
transition-duration: 2s;
|
||||
transition-delay: 0s;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
#rankings span.teamname:hover {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#rankings span.teampoints {
|
||||
font-size:100%;
|
||||
height:1.2em;
|
||||
margin:0em;
|
||||
padding:0em;
|
||||
width:99%;
|
||||
}
|
||||
|
||||
#rankings .team {
|
||||
transition-property: all;
|
||||
transition-duration: 1s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
|
||||
#rankings div * {white-space: nowrap;}
|
||||
.cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;}
|
||||
.cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;}
|
||||
.cat2, .cat10, .cat18 {background-color: #b2df8a; color: black;}
|
||||
.cat3, .cat11, .cat19 {background-color: #33a02c; color: white;}
|
||||
.cat4, .cat12, .cat20 {background-color: #fb9a99; color: black;}
|
||||
.cat5, .cat13, .cat21 {background-color: #e31a1c; color: white;}
|
||||
.cat6, .cat14, .cat22 {background-color: #fdbf6f; color: black;}
|
||||
.cat7, .cat15, .cat23 {background-color: #ff7f00; color: black;}
|
||||
|
||||
|
||||
#rankings .teampoints.inv { text-align: center;}
|
||||
|
||||
.hidden { visibility: hidden; }
|
||||
|
||||
/* Responsive design */
|
||||
/* Defaults */
|
||||
#rankings span.teampoints {
|
||||
max-width:89%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:10%;
|
||||
min-width:10%;
|
||||
}
|
||||
|
||||
|
||||
/* Monitors with large enough screens to do side by side */
|
||||
@media only screen and (min-width: 170em) {
|
||||
#chart, #rankings {
|
||||
width: 49%;
|
||||
display:inline-block;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Monitor
|
||||
@media only screen and (max-width: 130em) {
|
||||
#chart, #rankings {
|
||||
width: 49%;
|
||||
display:inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#rankings span.teampoints {
|
||||
max-width:89%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:10%;
|
||||
min-width:10%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Laptop size screen */
|
||||
@media only screen and (max-width: 100em) {
|
||||
#rankings span.teampoints {
|
||||
max-width:84%;
|
||||
}
|
||||
#rankings span.ueamname {
|
||||
max-width:15%;
|
||||
min-width:15%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Roughly Tablet size */
|
||||
@media only screen and (max-width: 70em) {
|
||||
#rankings span.teampoints {
|
||||
max-width:79%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:20%;
|
||||
min-width:20%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small screens phone size */
|
||||
@media only screen and (max-width: 40em) {
|
||||
#rankings span.teampoints {
|
||||
max-width:65%;
|
||||
}
|
||||
#rankings span.teamname {
|
||||
max-width:34%;
|
||||
min-width:34%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#devel {
|
||||
background-color: #eee;
|
||||
color: black;
|
||||
overflow: scroll;
|
||||
}
|
||||
#devel .string {
|
||||
color: #9c27b0;
|
||||
}
|
||||
#devel .body {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
.kvpair {
|
||||
border: solid black 2px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: block;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin: 1px;
|
||||
border-radius: 50%;
|
||||
border: 5px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: rotate 1.2s linear infinite;
|
||||
}
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
li[draggable]::before {
|
||||
content: "↕";
|
||||
padding: 0.5em;
|
||||
cursor: move;
|
||||
}
|
||||
li[draggable] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
[draggable].moving {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
[draggable].over {
|
||||
border: 1px white dashed;
|
||||
}
|
||||
|
||||
#cacheButton.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#settings {
|
||||
position: fixed;
|
||||
top: 0.5em;
|
||||
right: 1em;
|
||||
opacity: 1;
|
||||
background-blend-mode: darken;
|
||||
margin:0em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
transition-property: all;
|
||||
transition-duration: 1s;
|
||||
transition-delay: 0s;
|
||||
cursor: pointer;
|
||||
z-index:10000;
|
||||
padding-bottom: 1em;
|
||||
list-style: none;
|
||||
border-top-left-radius: 0.5em;
|
||||
border-bottom-left-radius: 0.5em;
|
||||
border-top-right-radius: 0.5em;
|
||||
border-bottom-right-radius: 0.5em;
|
||||
padding:0em;
|
||||
border: thin solid transparent;
|
||||
}
|
||||
|
||||
#settings ul {
|
||||
padding:0em;
|
||||
margin: 0.75em 0em;
|
||||
}
|
||||
|
||||
#settings ul > li {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#settings .btn {
|
||||
background-color:#33f;
|
||||
padding: 0.5em 0.5em;;
|
||||
vertical-align: middle;
|
||||
border-top-left-radius: 0.5em;
|
||||
border-top-right-radius: 0.5em;
|
||||
border-bottom-left-radius: 0.5em;
|
||||
border-bottom-right-radius: 0.5em;
|
||||
display:inline-block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#settings > li {
|
||||
display: none;
|
||||
width: 95%;
|
||||
text-align: left;
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
|
||||
#settings:hover {
|
||||
border: thin solid #66f;
|
||||
background-color: #292929;
|
||||
width: 15em;
|
||||
padding-left: 0.5em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
#settings:hover li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#settings label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#rankings .category {
|
||||
transition-property: margin, width, offset;
|
||||
transition-duration: 1s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#rankHeading {
|
||||
visibility:hidden
|
||||
}
|
||||
|
||||
.track #rankHeading {
|
||||
visibility:visible;
|
||||
}
|
||||
|
||||
.leader .icon {
|
||||
background-repeat: no-repeat;
|
||||
background-color: transparent;
|
||||
background-origin: padding-box;
|
||||
background-size: contain;
|
||||
padding-left: 1.5em;
|
||||
height: 1em;
|
||||
float: left;
|
||||
display:none;
|
||||
}
|
||||
|
||||
.fun .leader .icon {
|
||||
display:inline-block !important;
|
||||
}
|
||||
|
||||
.icon.track-entry-point {
|
||||
background-image: url("scoreboard-assets/entry-point.png");
|
||||
}
|
||||
|
||||
.icon.track-forensics {
|
||||
background-image: url("scoreboard-assets/forensics.png");
|
||||
}
|
||||
.icon.track-incident-coordination {
|
||||
background-image: url("scoreboard-assets/incident-coordination.png");
|
||||
}
|
||||
|
||||
.icon.track-malware {
|
||||
background-image: url("scoreboard-assets/malware.png");
|
||||
}
|
||||
|
||||
.icon.track-netarch {
|
||||
background-image: url("scoreboard-assets/netarch.png");
|
||||
}
|
||||
|
||||
.icon.track-ot {
|
||||
background-image: url("scoreboard-assets/ot.png");
|
||||
}
|
||||
|
||||
/* highlight row on hover so it is easier for users to see */
|
||||
#rankings > div {
|
||||
border-bottom: thin solid transparent;
|
||||
}
|
||||
|
||||
#rankings > div:hover {
|
||||
font-weight: bold;
|
||||
/* border-bottom: thin solid #33f !important;*/
|
||||
background-color: #779 !important;
|
||||
}
|
||||
|
||||
#screensaver {
|
||||
width:110vw;
|
||||
height:110vh;
|
||||
position:fixed;
|
||||
top:-10em;
|
||||
left:-10em;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
background-image:url('luna-moth.svg');
|
||||
background-blend-mode:multiply;
|
||||
background-size: 10em 10em;
|
||||
background-repeat: space;
|
||||
transition-property: all;
|
||||
transition-duration: 5s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#screensaverImg {
|
||||
width:10em;
|
||||
position: fixed;
|
||||
top:0em;
|
||||
left:0em;
|
||||
}
|
|
@ -2,23 +2,47 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Scoreboard</title>
|
||||
<link rel="stylesheet" href="basic.css">
|
||||
<link rel="stylesheet" href="scoreboard-base.css">
|
||||
<link rel="stylesheet" href="gibson.css">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<script src="moment.min.js" async></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" async></script>
|
||||
<script src="scoreboard.js" async></script>
|
||||
</head>
|
||||
<body class="wide">
|
||||
<h4 id="location"></h4>
|
||||
<ul id="settings">
|
||||
<em class="btn">Settings</em>
|
||||
<li>
|
||||
<ul>Ranking Format
|
||||
<li><input type="radio" id="rankingStandard" name="rankingPerspective" value="standard" checked /> <label for="rankingStandard">Standard</label></li>
|
||||
<li><input type="radio" id="rankingCategory" name="rankingPerspective" value="category" /> <label for="rankingCategory">Category</label></li>
|
||||
<li><input type="radio" id="rankingTrack" name="rankingPerspective" value="track"/> <label for="rankingTrack">Track (<em>placeholder</em>)</label></li>
|
||||
</ul>
|
||||
<ul>Features
|
||||
<li><input type="checkbox" id="mothLeaderIcons" name="mothLeaderIcons" value="true" /> <label for="mothLeaderIcons">Show Leader icons</label></li>
|
||||
<li><input type="checkbox" id="mothScreenSaver" name="mothScreenSaver" value="true"/> <label for="mothScreenSaver">Moth ScreenSaver</label></li>
|
||||
<ul style="margin-left: 2em;">
|
||||
<li><input type="radio" id="mothScreenSaverBgNone" name="mothScreenSaverBg" value="transparent" /> <label for="mothScreenSaverBgNone">Transparent</label></li>
|
||||
<li><input type="radio" id="mothScreenSaverBgText" name="mothScreenSaverBg" value="svg" checked /> <label for="mothScreenSaverBgText">SVG/Text</label></li>
|
||||
<li><input type="radio" id="mothScreenSaverBgWindow" name="mothScreenSaverBg" value="window" /> <label for="mothScreenSaverBgWindow">Window</label></li>
|
||||
<li><input type="radio" id="mothScreenSaverBgCombined" name="mothScreenSaverBg" value="combined" /> <label for="mothScreenSaverBgCombined">Combined</label></li>
|
||||
</ul>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<section class="rotate">
|
||||
<div id="chart"></div>
|
||||
<div id="rankings"></div>
|
||||
</section>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="index.html">Puzzles</a></li>
|
||||
<li><a href="scoreboard.html">Scoreboard</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script src="moment.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"></script>
|
||||
<script src="scoreboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,8 +1,34 @@
|
|||
// jshint asi:true
|
||||
|
||||
var MOTH_RANKING_STANDARD=0;
|
||||
var MOTH_RANKING_CATEGORY=1;
|
||||
var MOTH_RANKING_TRACK=2;
|
||||
|
||||
// Comparison functions
|
||||
var MOTH_COMP_TEAMOVERALL=function (a, b) {
|
||||
return a.overallScore - b.overallScore;
|
||||
}
|
||||
|
||||
var MOTH_COMP_POINTSLOGTIME=function(a, b) {
|
||||
return a[0] - b[0];
|
||||
}
|
||||
|
||||
var MOTH_COMP_SCORE=function(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
var teamNames={};
|
||||
var pointsLog={};
|
||||
|
||||
function scoreboardInit() {
|
||||
// Visual flare
|
||||
var cortex={
|
||||
ranking: MOTH_RANKING_STANDARD,
|
||||
sorting: MOTH_COMP_TEAMOVERALL,
|
||||
screenSaverBg: "svg",
|
||||
};
|
||||
|
||||
chartColors = [
|
||||
var chartColors = [
|
||||
"rgb(255, 99, 132)",
|
||||
"rgb(255, 159, 64)",
|
||||
"rgb(255, 205, 86)",
|
||||
|
@ -11,7 +37,19 @@ function scoreboardInit() {
|
|||
"rgb(153, 102, 255)",
|
||||
"rgb(201, 203, 207)"
|
||||
]
|
||||
|
||||
|
||||
// Placeholder for Track mappings
|
||||
trackMap = {
|
||||
"Operational-Technology" : "ot",
|
||||
"Safe_Malware" : "malware",
|
||||
"linux_memory_intro" : "forensics",
|
||||
"js" : "netarch",
|
||||
"sequence" : "netarch",
|
||||
"networking" : "entry-point",
|
||||
"codebreaking" : "incident-coordination",
|
||||
"nocode" : "entry-point",
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
window.state = state
|
||||
|
||||
|
@ -19,10 +57,9 @@ function scoreboardInit() {
|
|||
rotate.appendChild(rotate.firstElementChild)
|
||||
}
|
||||
|
||||
let element = document.getElementById("rankings")
|
||||
let teamNames = state.TeamNames
|
||||
let pointsLog = state.PointsLog
|
||||
|
||||
teamNames = state.TeamNames
|
||||
pointsLog = state.PointsLog
|
||||
|
||||
// Every machine that's displaying the scoreboard helpfully stores the last 20 values of
|
||||
// points.json for us, in case of catastrophe. Thanks, y'all!
|
||||
//
|
||||
|
@ -34,64 +71,68 @@ function scoreboardInit() {
|
|||
}
|
||||
stateHistory.push(state)
|
||||
localStorage.setItem("stateHistory", JSON.stringify(stateHistory))
|
||||
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
let teams = {}
|
||||
let categories = {} // map[string][team]int
|
||||
let highestCategoryScore = {} // map[string]int
|
||||
|
||||
|
||||
let uiRanking=cortex.ranking.valueOf()
|
||||
let element = document.getElementById("rankings")
|
||||
|
||||
|
||||
// Initialize data structures
|
||||
for (let teamId in teamNames) {
|
||||
teams[teamId] = {
|
||||
teams[teamId] = {
|
||||
categoryScore: {}, // map[string]int
|
||||
trackScore: {}, // map[string]int
|
||||
overallScore: 0, // int
|
||||
historyLine: [], // []{x: int, y: int}
|
||||
history: [], // []{t: timestamp, c: category, s: int}
|
||||
name: teamNames[teamId],
|
||||
id: teamId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Dole out points
|
||||
pointsLog.sort(MOTH_COMP_POINTSLOGTIME)
|
||||
|
||||
for (let entry of pointsLog) {
|
||||
let timestamp = entry[0]
|
||||
let teamId = entry[1]
|
||||
let category = entry[2]
|
||||
let points = entry[3]
|
||||
let points = parseInt(entry[3])
|
||||
|
||||
let team = teams[teamId]
|
||||
|
||||
|
||||
let score = team.categoryScore[category] || 0
|
||||
let trackScore = team.trackScore[trackMap[category]] || 0
|
||||
score += points
|
||||
trackScore += points
|
||||
|
||||
team.categoryScore[category] = score
|
||||
|
||||
let highest = highestCategoryScore[category] || 0
|
||||
if (score > highest) {
|
||||
highestCategoryScore[category] = score
|
||||
|
||||
team.history.push({t: new Date(timestamp * 1000), cat: category, score: points})
|
||||
|
||||
if (!categories[category]) {
|
||||
categories[category]={}
|
||||
}
|
||||
|
||||
categories[category][teamId]=score
|
||||
}
|
||||
|
||||
// Search Team score aggregates for highest scores and key markers
|
||||
for (let cat in categories) {
|
||||
let scores=Object.values(categories[cat])
|
||||
scores.sort(MOTH_COMP_SCORE)
|
||||
scores.reverse();
|
||||
let highest=scores[0]
|
||||
highestCategoryScore[cat]=highest.valueOf()
|
||||
}
|
||||
|
||||
for (let teamId in teamNames) {
|
||||
teams[teamId].categoryScore = {}
|
||||
}
|
||||
|
||||
for (let entry of pointsLog) {
|
||||
let timestamp = entry[0]
|
||||
let teamId = entry[1]
|
||||
let category = entry[2]
|
||||
let points = entry[3]
|
||||
|
||||
let team = teams[teamId]
|
||||
|
||||
let score = team.categoryScore[category] || 0
|
||||
score += points
|
||||
team.categoryScore[category] = score
|
||||
|
||||
let overall = 0
|
||||
for (let cat in team.categoryScore) {
|
||||
overall += team.categoryScore[cat] / highestCategoryScore[cat]
|
||||
}
|
||||
|
||||
team.historyLine.push({t: new Date(timestamp * 1000), y: overall})
|
||||
}
|
||||
|
||||
// Compute overall scores based on current highest
|
||||
for (let teamId in teams) {
|
||||
let team = teams[teamId]
|
||||
|
@ -99,19 +140,24 @@ function scoreboardInit() {
|
|||
for (let cat in team.categoryScore) {
|
||||
team.overallScore += team.categoryScore[cat] / highestCategoryScore[cat]
|
||||
}
|
||||
|
||||
// HistoryLine
|
||||
let overall = 0
|
||||
for (let history in team.history) {
|
||||
let entry=team.history[history]
|
||||
overall+=entry.score/highestCategoryScore[entry.cat]
|
||||
team.historyLine.push({t: entry.t, y: overall.toFixed(2)})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Sort by team score
|
||||
function teamCompare(a, b) {
|
||||
return a.overallScore - b.overallScore
|
||||
}
|
||||
|
||||
|
||||
// Figure out how to order each team on the scoreboard
|
||||
let winners = []
|
||||
for (let teamId in teams) {
|
||||
winners.push(teams[teamId])
|
||||
}
|
||||
winners.sort(teamCompare)
|
||||
winners.sort(cortex.sorting)
|
||||
winners.reverse()
|
||||
|
||||
// Let's make some better names for things we've computed
|
||||
|
@ -121,30 +167,88 @@ function scoreboardInit() {
|
|||
// Clear out the element we're about to populate
|
||||
Array.from(element.childNodes).map(e => e.remove())
|
||||
|
||||
let maxWidth = 100 / winningScore
|
||||
let maxWidth = (100 / winningScore)
|
||||
let avgWidth = (100 / numCategories)
|
||||
|
||||
// Pre-load heading
|
||||
let headingRow=document.createElement("div")
|
||||
headingRow.id="rankHeading";
|
||||
let headingRowPoints=document.createElement("span")
|
||||
headingRowPoints.classList.add("teampoints")
|
||||
headingRowPoints.classList.add("inv")
|
||||
|
||||
let headingNcat=0
|
||||
for (let category in highestCategoryScore) {
|
||||
let bar=document.createElement("span")
|
||||
bar.title=category
|
||||
bar.style.width=avgWidth +"%"
|
||||
bar.classList.add("cat" +headingNcat)
|
||||
bar.textContent=category
|
||||
bar.dataset.category=category;
|
||||
bar.onclick=sortByCategory;
|
||||
|
||||
headingRowPoints.appendChild(bar);
|
||||
headingNcat+=1
|
||||
}
|
||||
|
||||
headingRow.appendChild(headingRowPoints);
|
||||
element.appendChild(headingRow)
|
||||
|
||||
for (let team of winners) {
|
||||
let row = document.createElement("div")
|
||||
row.classList.add("team");
|
||||
row.dataset.overallScore=team.overallScore.toFixed(2);
|
||||
|
||||
let ncat = 0
|
||||
|
||||
let teamPoints=document.createElement("span")
|
||||
teamPoints.classList.add("teampoints")
|
||||
|
||||
let leader=[];
|
||||
|
||||
for (let category in highestCategoryScore) {
|
||||
let catHigh = highestCategoryScore[category]
|
||||
let catTeam = team.categoryScore[category] || 0
|
||||
let catPct = catTeam / catHigh
|
||||
let width = maxWidth * catPct
|
||||
let width = (maxWidth * catPct)
|
||||
let catWidth = (avgWidth * catPct)
|
||||
|
||||
let bar = document.createElement("span")
|
||||
bar.classList.add("category")
|
||||
bar.classList.add("cat" + ncat)
|
||||
bar.style.width = width + "%"
|
||||
bar.textContent = category + ": " + catTeam
|
||||
bar.title = bar.textContent
|
||||
bar.dataset.standardWidth=width;
|
||||
bar.dataset.categoryWidth=catWidth;
|
||||
bar.dataset.category=category;
|
||||
bar.dataset.points=catTeam;
|
||||
bar.dataset.categoryMargin=(avgWidth - catWidth)
|
||||
bar.title = bar.dataset.category + ": " + bar.dataset.points
|
||||
|
||||
if ((catTeam == catHigh) && (trackMap[category])){
|
||||
leader.push(trackMap[category])
|
||||
}
|
||||
|
||||
displayMothRanking(uiRanking, bar);
|
||||
|
||||
row.appendChild(bar)
|
||||
teamPoints.appendChild(bar)
|
||||
ncat += 1
|
||||
}
|
||||
|
||||
row.appendChild(teamPoints)
|
||||
|
||||
let te = document.createElement("span")
|
||||
te.classList.add("teamname")
|
||||
te.textContent = team.name
|
||||
|
||||
for (let track in leader) {
|
||||
te.classList.add("leader");
|
||||
|
||||
let img=document.createElement("img");
|
||||
img.classList.add("icon");
|
||||
img.classList.add("track-"+leader[track]);
|
||||
|
||||
te.prepend(img);
|
||||
}
|
||||
|
||||
row.appendChild(te)
|
||||
|
||||
element.appendChild(row)
|
||||
|
@ -230,22 +334,329 @@ function scoreboardInit() {
|
|||
})
|
||||
}
|
||||
|
||||
let imgRL=null;
|
||||
let imgLR=null;
|
||||
let canvas=null;
|
||||
|
||||
function setScreenSaver() {
|
||||
let uiScreenSaver=document.querySelector("#mothScreenSaver");
|
||||
|
||||
if (uiScreenSaver.checked) {
|
||||
if (canvas == null) {
|
||||
canvas=document.createElement("picture");
|
||||
canvas.id="gibson"
|
||||
}
|
||||
|
||||
document.querySelector("body").appendChild(canvas);
|
||||
|
||||
if (imgRL == null) {
|
||||
imgRL=document.createElement("img");
|
||||
imgRL.classList.add("moth");
|
||||
imgRL.src="luna-moth.svg";
|
||||
imgRL.style.animation="slideRL 4s linear infinite";
|
||||
imgRL.dataset.direction="RL";
|
||||
imgRL.addEventListener("animationiteration", lunaRLIterListener, false);
|
||||
}
|
||||
|
||||
imgRL.style.top="4em";
|
||||
|
||||
if (imgLR == null) {
|
||||
imgLR=document.createElement("img");
|
||||
imgLR.classList.add("moth");
|
||||
imgLR.src="luna-moth.svg";
|
||||
imgLR.style.animation="slideLR 4s linear infinite";
|
||||
imgLR.dataset.direction="LR";
|
||||
imgLR.addEventListener("animationiteration", lunaLRIterListener, false);
|
||||
}
|
||||
|
||||
imgLR.style.top=0;
|
||||
|
||||
canvas.appendChild(imgLR);
|
||||
canvas.appendChild(imgRL);
|
||||
|
||||
setScreenSaverBg();
|
||||
|
||||
setTimeout(lunaShadow, Math.random()*100+400);
|
||||
|
||||
} else {
|
||||
canvas=document.querySelector("#gibson");
|
||||
if (canvas) {
|
||||
canvas.remove();
|
||||
}
|
||||
|
||||
canvas=null;
|
||||
imgRL=null;
|
||||
imgLR=null;
|
||||
}
|
||||
}
|
||||
|
||||
function setScreenSaverBg() {
|
||||
let options=["svg", "window", "combined"];
|
||||
document.querySelectorAll("input[name=mothScreenSaverBg]").forEach(function(item) {
|
||||
if (item.checked === true) {
|
||||
cortex.screenSaverBg=item.value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (canvas !== null) {
|
||||
for (let i=0; i<options.length; i++) {
|
||||
if (options[i] != cortex.screenSaverBg) {
|
||||
canvas.classList.remove(options[i]);
|
||||
}
|
||||
|
||||
canvas.classList.add(cortex.screenSaverBg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function animateScreenSaver() {
|
||||
let img=document.querySelector("#screensaverImg");
|
||||
|
||||
if (img == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let coord=img.getBoundingClientRect();
|
||||
|
||||
img.style.transitionDuration="0s"
|
||||
img.style.transform="translate("+ Math.max((-1*coord.width), (-1*(coord.width+coord.x))) + "px, " + (coord.y + coord.height) + "px) rotate(135deg)";
|
||||
|
||||
setTimeout(function() {
|
||||
img.style.transitionDuration="5s"
|
||||
img.style.transform="translate(100vw, " + (coord.y + coord.height) + "px) rotate(135deg)";
|
||||
}, 100);
|
||||
|
||||
setTimeout(animateScreenSaver, 5200);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function setShowLeaderIcons() {
|
||||
let uiShowLeaderIcons=document.querySelector("#mothLeaderIcons");
|
||||
let body=document.querySelector("body");
|
||||
|
||||
(uiShowLeaderIcons.checked)? body.classList.add("fun") : body.classList.remove("fun");
|
||||
|
||||
}
|
||||
|
||||
function setRankingTrack() {
|
||||
let uiRankingType=document.querySelector("input[name=rankingPerspective]:checked");
|
||||
|
||||
switch (uiRankingType.value) {
|
||||
case "track":
|
||||
cortex.ranking=MOTH_RANKING_TRACK;
|
||||
break;
|
||||
|
||||
case "category":
|
||||
cortex.ranking=MOTH_RANKING_CATEGORY;
|
||||
break;
|
||||
|
||||
case "standard":
|
||||
default:
|
||||
cortex.ranking=MOTH_RANKING_STANDARD;
|
||||
}
|
||||
|
||||
let uiRanking=cortex.ranking.valueOf();
|
||||
|
||||
document.querySelectorAll(".category").forEach(function(item) {
|
||||
displayMothRanking(uiRanking, item)
|
||||
});
|
||||
|
||||
switch (uiRanking) {
|
||||
case MOTH_RANKING_CATEGORY:
|
||||
case MOTH_RANKING_TRACK:
|
||||
document.querySelector("#rankings").classList.add("track");
|
||||
break;
|
||||
|
||||
case MOTH_RANKING_STANDARD:
|
||||
default:
|
||||
cortex.sorting = MOTH_COMP_TEAMOVERALL;
|
||||
setTimeout(sortByCategory, 1000);
|
||||
document.querySelector("#rankings").classList.remove("track");
|
||||
}
|
||||
}
|
||||
|
||||
function displayMothRanking(rankingStyle, obj) {
|
||||
switch (rankingStyle) {
|
||||
case MOTH_RANKING_CATEGORY:
|
||||
case MOTH_RANKING_TRACK:
|
||||
obj.style.width=obj.dataset.categoryWidth + "%";
|
||||
obj.style.marginRight=obj.dataset.categoryMargin + "%";
|
||||
obj.textContent = obj.dataset.points;
|
||||
break;
|
||||
|
||||
case MOTH_RANKING_STANDARD:
|
||||
default:
|
||||
obj.style.width=obj.dataset.standardWidth + "%";
|
||||
obj.style.marginRight=0+"%";
|
||||
obj.textContent = obj.dataset.category + ": " + obj.dataset.points
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Sort winners by category score
|
||||
function sortByCategory() {
|
||||
let cat=(this.dataset)? this.dataset.category || MOTH_COMP_TEAMOVERALL : MOTH_COMP_TEAMOVERALL;
|
||||
|
||||
let teamOrder=Array.from(document.querySelectorAll("#rankings div.team"));
|
||||
let rankings=document.querySelector("#rankings");
|
||||
|
||||
// Grab current screen positions of objects
|
||||
let coords=[]
|
||||
|
||||
for (let i=0; i<teamOrder.length; i++) {
|
||||
coords.push({ x: teamOrder[i].offsetLeft, y: teamOrder[i].offsetTop });
|
||||
}
|
||||
|
||||
// console.log(teamOrder[0].offsetTop);
|
||||
|
||||
// teamOrder.forEach(function(item) {
|
||||
// item.style.top=item.offsetTop +"px";
|
||||
// item.style.left=item.offsetLeft +"px";
|
||||
// });
|
||||
|
||||
if (cat == MOTH_COMP_TEAMOVERALL) {
|
||||
teamOrder.sort(function(a, b) {
|
||||
return b.dataset.overallScore - a.dataset.overallScore;
|
||||
});
|
||||
|
||||
} else {
|
||||
teamOrder.sort(function(a, b) {
|
||||
let aa=a.querySelector("span[data-category=" + cat + "]") || { dataset : {points: 0}};
|
||||
let bb=b.querySelector("span[data-category=" + cat + "]") || { dataset : {points: 0}};
|
||||
|
||||
return bb.dataset.points - aa.dataset.points;
|
||||
});
|
||||
}
|
||||
|
||||
// Move elements without rearranging the DOM
|
||||
// Seems to be a couple ways to do this.
|
||||
// 1. appendChild in DOM will move the element without transition
|
||||
// 2. translate to move the elements where they should be with transition
|
||||
teamOrder.forEach(function(item, i) {
|
||||
item.style.transform="translate(" + (coords[i]["x"] - item.offsetLeft) + "px, " + (coords[i]["y"] - item.offsetTop) +"px)"
|
||||
|
||||
|
||||
// rankings.appendChild(item);
|
||||
// item.style.transform="translate(" + (item-offsetLeft - coords[i]["x"]) + "px, " + (item.offsetTop - coords[i]["y"]) +"px)"
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/* Luna Moth ScreenSaver */
|
||||
function lunaRLIterListener(event) {
|
||||
let coord=imgRL.getBoundingClientRect();
|
||||
|
||||
if ((coord.y + coord.height) > canvas.clientHeight) {
|
||||
// Repeat in infinite loop
|
||||
// imgRL.style.top="4em";
|
||||
|
||||
// Uncomment to run through screen once
|
||||
imgRL.style.animationPlayState="paused";
|
||||
} else {
|
||||
imgRL.style.top=(coord.y + (2*coord.height))+"px";
|
||||
}
|
||||
}
|
||||
|
||||
function lunaLRIterListener(event) {
|
||||
let coord=imgLR.getBoundingClientRect();
|
||||
|
||||
if ((coord.y + coord.height) > canvas.clientHeight) {
|
||||
// Repeat in infinite loop
|
||||
// imgLR.style.top="0px";
|
||||
|
||||
// Uncomment to run through screen once
|
||||
imgLR.style.animationPlayState="paused";
|
||||
} else {
|
||||
imgLR.style.top=(coord.y + (2*coord.height))+"px";
|
||||
}
|
||||
}
|
||||
|
||||
function lunaShadow() {
|
||||
// let coordCanvas=canvas.getBoundingClientRect();
|
||||
let targets=[imgRL, imgLR];
|
||||
|
||||
targets.forEach(function(item) {
|
||||
if (item == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let coord=item.getBoundingClientRect();
|
||||
let div=document.createElement("div");
|
||||
div.className="shadow "+item.dataset.direction;
|
||||
div.style.top=item.style.top;
|
||||
div.style.left=coord.left +"px";
|
||||
|
||||
canvas.appendChild(div);
|
||||
});
|
||||
|
||||
// Only run through screen once
|
||||
if ((imgRL != null) && (imgRL.style.animationPlayState != "paused")) {
|
||||
setTimeout(lunaShadow, Math.random()*400+200);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function init() {
|
||||
let base = window.location.href.replace("scoreboard.html", "")
|
||||
let location = document.querySelector("#location")
|
||||
let params = new URLSearchParams(document.location.search.substring(1));
|
||||
|
||||
if (location) {
|
||||
location.textContent = base
|
||||
}
|
||||
|
||||
|
||||
// Grab initial settings and set event handlers
|
||||
document.querySelectorAll("input[name=rankingPerspective]").forEach(function(item) {
|
||||
if (item.value == params.get(item.name)) {
|
||||
item.checked=true;
|
||||
}
|
||||
item.onchange=setRankingTrack;
|
||||
});;
|
||||
|
||||
// Leader Icons
|
||||
let leaderIcons=document.querySelector("#mothLeaderIcons");
|
||||
if (leaderIcons) {
|
||||
leaderIcons.checked=(params.get(leaderIcons.name) == leaderIcons.value);
|
||||
leaderIcons.onchange=setShowLeaderIcons;
|
||||
}
|
||||
|
||||
// Screensaver
|
||||
let screenSaver=document.querySelector("#mothScreenSaver");
|
||||
if (screenSaver) {
|
||||
screenSaver.checked=(params.get(screenSaver.name) == screenSaver.value);
|
||||
screenSaver.onchange=setScreenSaver;
|
||||
}
|
||||
|
||||
document.querySelectorAll("input[name=mothScreenSaverBg]").forEach(function(item) {
|
||||
if (item.value == params.get(item.name)) {
|
||||
item.checked=true;
|
||||
}
|
||||
|
||||
item.onchange=setScreenSaverBg;;
|
||||
});
|
||||
|
||||
setRankingTrack();
|
||||
setShowLeaderIcons();
|
||||
setScreenSaver();
|
||||
|
||||
setInterval(refresh, 60000)
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", scoreboardInit)
|
||||
} else {
|
||||
scoreboardInit()
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue