this commit is a lie
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
# Ignore copyrighted resources, get them at https://www.starstable.com/article/8934
|
||||||
|
src/background.jpg
|
||||||
|
src/favicon-32x32.png
|
||||||
|
|
||||||
|
# Ignore the processed data to make sure there aren't multiple sources of truth
|
||||||
|
data/championships.json
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Star Stable Timer
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Put your favicon to `src/favicon-32x32.png` and your page background to `src/background.jpg`
|
||||||
|
Perform the championship time update.
|
||||||
|
|
||||||
|
## Updating Championship Times
|
||||||
|
|
||||||
|
[The official site](https://www.starstable.com/game/championships) has the times published as images, so it isn't really possible to fetch them without OCR, and even then the publised times are quite out of date usually...
|
||||||
|
|
||||||
|
Since most other sources are outdated too, you will have to hand-type your championship times. Save them into `data/championships.txt` in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
The Something Championship
|
||||||
|
|
||||||
|
Monday: 00:10, 02:00, 03:30
|
||||||
|
Tuesday: 09:00, 10:00
|
||||||
|
|
||||||
|
Somewhere Else
|
||||||
|
|
||||||
|
Tuesday: 01:30, 01:50, 01:59
|
||||||
|
Wednesday: 02:00, 03:00, 04:00
|
||||||
|
```
|
||||||
|
|
||||||
|
Then execute:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd data
|
||||||
|
php championships-to-json.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the command completes, you have the latest schedule ready to use.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// much faster than file()
|
||||||
|
$lines = explode(
|
||||||
|
"\n",
|
||||||
|
str_replace(
|
||||||
|
["\r\n", "\r"],
|
||||||
|
"\n",
|
||||||
|
file_get_contents('championships.txt')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$location = "Unknown";
|
||||||
|
$championships_by_time = [];
|
||||||
|
|
||||||
|
// get the data into a per-event format
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line)) continue;
|
||||||
|
|
||||||
|
if (str_contains($line, ":")) {
|
||||||
|
$day_and_times = explode(": ", $line);
|
||||||
|
$day = trim($day_and_times[0]);
|
||||||
|
foreach (explode(",", trim($day_and_times[1])) as $time) {
|
||||||
|
$championships_by_time[] = [$day, trim($time), $location];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$location = str_replace(["The ", " Championship"], "", $line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// native JS expects dates in this format
|
||||||
|
$days = [
|
||||||
|
'Monday' => 1,
|
||||||
|
'Tuesday' => 2,
|
||||||
|
'Wednesday' => 3,
|
||||||
|
'Thursday' => 4,
|
||||||
|
'Friday' => 5,
|
||||||
|
'Saturday' => 6,
|
||||||
|
'Sunday' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$championships_by_days = [];
|
||||||
|
|
||||||
|
foreach ($championships_by_time as $championship) {
|
||||||
|
[$day_raw, $time_raw, $location] = $championship;
|
||||||
|
|
||||||
|
$day = $days[$day_raw];
|
||||||
|
|
||||||
|
$time_components = explode(':', $time_raw);
|
||||||
|
$time = 60*intval($time_components[0]) + intval($time_components[1]);
|
||||||
|
|
||||||
|
if (!isset($championships_by_days[$day])) {
|
||||||
|
$championships_by_days[$day] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$championships_by_days[$day][$time] = $location;
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional, but it's compile time so sorting doesn't really hurt performance
|
||||||
|
foreach ($championships_by_days as $key => $_value) { ksort($championships_by_days[$key]); }
|
||||||
|
|
||||||
|
$json = json_encode($championships_by_days, JSON_PRETTY_PRINT) . "\n";
|
||||||
|
file_put_contents("championships.json", $json);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
This is just a sample, and it might be updated!
|
||||||
|
|
||||||
|
The Firgrove Championship
|
||||||
|
|
||||||
|
Monday: 03:30, 09:30, 15:30, 21:30
|
||||||
|
Tuesday: 03:00, 09:00, 15:00, 21:00
|
||||||
|
Wednesday: 02:30, 08:30, 14:30, 20:30
|
||||||
|
Thursday: 02:00, 08:00, 12:00, 14:00, 20:00
|
||||||
|
Friday: 01:00, 07:00, 13:00, 19:00
|
||||||
|
Saturday: 00:00, 06:00, 10:00, 15:00, 19:00
|
||||||
|
Sunday: 04:00, 09:00, 17:30, 23:00
|
||||||
|
|
||||||
|
The Fort Pinta Championship
|
||||||
|
|
||||||
|
Monday: 00:30, 04:30, 08:30, 12:30, 16:30, 18:00, 20:30
|
||||||
|
Tuesday: 00:00, 04:00, 08:00, 12:00, 16:00, 20:00
|
||||||
|
Wednesday: 03:30, 07:30, 11:30, 15:30, 19:30, 23:30
|
||||||
|
Thursday: 03:00, 07:00, 11:00, 15:00, 19:00, 23:00
|
||||||
|
Friday: 02:00, 06:00, 10:00, 14:00, 18:00, 22:00
|
||||||
|
Saturday: 01:30, 07:30, 12:00, 16:30, 21:00
|
||||||
|
Sunday: 00:30, 05:00, 06:30, 11:00, 15:30, 20:00
|
||||||
|
|
||||||
|
The Jorvik Stables Championship
|
||||||
|
|
||||||
|
Monday: 02:30, 07:00, 14:30, 22:30
|
||||||
|
Tuesday: 02:00, 06:30, 14:00, 22:00
|
||||||
|
Wednesday: 01:30, 06:00, 13:30, 21:30
|
||||||
|
Thursday: 01:00, 05:30, 13:00, 21:00
|
||||||
|
Friday: 00:00, 04:30, 12:00, 15:30, 20:00
|
||||||
|
Saturday: 03:30, 06:30, 10:30, 17:30, 19:30
|
||||||
|
Sunday: 02:30, 09:30, 14:00, 18:30
|
||||||
|
|
||||||
|
The Moorland Championship
|
||||||
|
|
||||||
|
Monday: 00:00, 04:00, 08:00, 12:00, 16:00, 20:00
|
||||||
|
Tuesday: 03:30, 07:30, 11:30, 15:30, 19:30, 23:30
|
||||||
|
Wednesday: 03:00, 07:00, 11:00, 15:00, 17:00, 19:00, 23:00
|
||||||
|
Thursday: 02:30, 06:30, 10:30, 14:30, 18:30, 22:30
|
||||||
|
Friday: 01:30, 05:30, 09:30, 13:30, 17:30, 21:30
|
||||||
|
Saturday: 01:00, 07:00, 11:30, 16:00, 20:30
|
||||||
|
Sunday: 00:00, 04:30, 06:00, 10:30, 15:00, 19:30
|
||||||
|
|
||||||
|
The Silversong Pony Championship
|
||||||
|
|
||||||
|
Monday: 01:00, 05:00, 09:00, 13:00, 17:00, 21:00
|
||||||
|
Tuesday: 00:30, 04:30, 08:30, 12:30, 16:30, 20:30
|
||||||
|
Wednesday: 00:00, 04:00, 08:00, 12:00, 16:00, 20:00
|
||||||
|
Thursday: 03:30, 07:30, 11:30, 15:30, 19:30, 23:30
|
||||||
|
Friday: 02:30, 06:30, 10:30, 14:30, 16:30, 18:30, 22:30
|
||||||
|
Saturday: 02:00, 08:00, 12:30, 17:00, 21:30
|
||||||
|
Sunday: 01:00, 05:30, 07:00, 11:30, 16:00, 20:30
|
||||||
|
|
||||||
|
The Silverglade Village Championship
|
||||||
|
|
||||||
|
Monday: 03:00, 07:30, 13:30, 17:30, 19:30
|
||||||
|
Tuesday: 02:30, 07:00, 17:00, 19:00
|
||||||
|
Wednesday: 02:00, 06:30, 16:30, 18:30
|
||||||
|
Thursday: 01:30, 06:00, 16:00, 18:00
|
||||||
|
Friday: 00:30, 05:00, 15:00, 17:00
|
||||||
|
Saturday: 04:00, 05:30, 14:00, 18:00, 23:00
|
||||||
|
Sunday: 03:00, 13:00, 18:00, 22:00
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SSO Timer</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="src/favicon-32x32.png">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="background-image"></div>
|
||||||
|
|
||||||
|
<div class="timer-container">
|
||||||
|
<div class="timer-panel">
|
||||||
|
<h2>Next Up</h2>
|
||||||
|
<h3 class="location-container">The <span id="location">...</span> Championship</h2>
|
||||||
|
<p class="championship">at <span id="time">00:00</span></p>
|
||||||
|
<p class="remaining">Remaining: <span id="timer">00:00:00</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="days" id="championships_table"></div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
var championships = {}
|
||||||
|
var lastChampionship = null
|
||||||
|
var locations = []
|
||||||
|
var updateFrequency = 500; // in ms
|
||||||
|
|
||||||
|
function flattenObjectIntoSingleUniqueArray(object) {
|
||||||
|
return [...new Set(Object.values(object).reduce((arr, t) => arr.concat(Object.values(t)), []))]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateObject() {
|
||||||
|
return new Date() // maybe easier to offset time zone later
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentDay() {
|
||||||
|
return getDateObject().getDay()
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTime() {
|
||||||
|
let date = getDateObject()
|
||||||
|
return date.getHours() * 60 + date.getMinutes()
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextDay() {
|
||||||
|
let day = currentDay() + 1
|
||||||
|
if (day > 6) day -= 6
|
||||||
|
|
||||||
|
return day
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstTimeOnDay(day) {
|
||||||
|
return Math.min(...Object.keys(championships[day]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstTimeNextDay() {
|
||||||
|
return firstTimeOnDay(nextDay())
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextTime() {
|
||||||
|
let time = currentTime()
|
||||||
|
|
||||||
|
let nextTimes = Object.keys(championships[currentDay()]).filter((t) => parseInt(t) >= time)
|
||||||
|
|
||||||
|
if (nextTimes.length < 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(...nextTimes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(time) {
|
||||||
|
return Math.floor(time/60).toString().padStart(2, '0') + ":" + (time % 60).toString().padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSeconds() {
|
||||||
|
return (60 - getDateObject().getSeconds()).toString().padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextChampionship() {
|
||||||
|
let remainingOffset = 0
|
||||||
|
let day = currentDay()
|
||||||
|
let time = nextTime()
|
||||||
|
|
||||||
|
if (time == null) {
|
||||||
|
remainingOffset = 1440
|
||||||
|
day = nextDay()
|
||||||
|
time = firstTimeNextDay()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
day: day,
|
||||||
|
time: time,
|
||||||
|
location: championships[day][time],
|
||||||
|
remaining: (remainingOffset + time) - currentTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChampionshipDetails(next) {
|
||||||
|
document.getElementById('time').innerHTML = formatTime(next.time)
|
||||||
|
document.getElementById('location').innerHTML = next.location
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimer(next) {
|
||||||
|
let remaining = formatTime(next.remaining) + ":" + formatSeconds()
|
||||||
|
document.getElementById('timer').innerHTML = remaining
|
||||||
|
document.title = `${remaining} - ${next.location} - SSO Timer`
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChampionshipTable(next) {
|
||||||
|
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
||||||
|
let table = ``
|
||||||
|
for (const [day, details] of Object.entries(championships)) {
|
||||||
|
let dayContainer = `<h1 class="day-title">${weekdays[day]}</h1>`
|
||||||
|
let dayContainerClasses = 'day-container'
|
||||||
|
|
||||||
|
if (currentDay() == day) {
|
||||||
|
dayContainerClasses += ' today'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [time, location] of Object.entries(details)) {
|
||||||
|
let formattedTime = formatTime(time)
|
||||||
|
let classes = 'time-container'
|
||||||
|
if (next.day == day && next.time == time) {
|
||||||
|
classes += ' next-time'
|
||||||
|
}
|
||||||
|
|
||||||
|
dayContainer += `<li class="${classes}"><b class="time">${formattedTime}</b>: ${location}</li>`
|
||||||
|
}
|
||||||
|
|
||||||
|
table += `<div class="${dayContainerClasses}"><ul class="times">${dayContainer}</ul></div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('championships_table').innerHTML = table
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
fetch('data/championships.json').then((response) => {
|
||||||
|
response.json().then((json) => {
|
||||||
|
championships = json
|
||||||
|
locations = flattenObjectIntoSingleUniqueArray(championships)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (championships.length < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = nextChampionship()
|
||||||
|
|
||||||
|
if (lastChampionship != nextChampionship()) {
|
||||||
|
updateChampionshipDetails(next)
|
||||||
|
updateChampionshipTable(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimer(next)
|
||||||
|
}, updateFrequency)
|
||||||
|
})
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
:root {
|
||||||
|
--border: black;
|
||||||
|
--sso-pink: #e53bb9;
|
||||||
|
--modal-background: rgba(255, 255, 255, 0.7);
|
||||||
|
--highlighted-modal-background: rgba(255, 210, 242, 0.7);
|
||||||
|
--highlighted-row: rgba(229, 59, 185, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
background-image: url('src/background.jpg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-container {
|
||||||
|
min-height: 600px;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-panel {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 40em;
|
||||||
|
margin: 5px auto;
|
||||||
|
background-color: var(--modal-background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-container {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.championship {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
background-color: var(--modal-background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today {
|
||||||
|
background-color: var(--highlighted-modal-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.times {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-container {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 3px 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-time {
|
||||||
|
background-color: var(--highlighted-row);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user