2026-01-16 02:30:44 +01:00
const API _URL = "https://api.splatcord.ink/prod/s1rotations" ;
function stageNames ( stages , lang = "en-US" ) {
return stages . map ( s => s . translatedNames [ lang ] ) ;
}
function stageImagePath ( stageName ) {
const firstWord = stageName
. split ( " " ) [ 0 ]
. toLowerCase ( )
. replace ( /['.]/g , "" ) ;
2026-01-17 22:19:07 +01:00
return ` /assets/stages/ ${ firstWord } .png ` ;
2026-01-16 02:30:44 +01:00
}
async function fetchRotations ( ) {
2026-01-16 21:59:39 +01:00
try {
const res = await fetch ( API _URL ) ;
if ( ! res . ok ) throw new Error ( ` HTTP error! status: ${ res . status } ` ) ;
const data = await res . json ( ) ;
const rotations = data ? . pretendo ? . rotations ? ? { } ;
const now = new Date ( ) ;
2026-01-22 23:08:26 -05:00
const timestamps = Object . keys ( rotations ) . sort ( ( a , b ) => Number ( a ) - Number ( b ) ) ;
2026-01-16 02:30:44 +01:00
2026-01-22 23:08:26 -05:00
const result = [ ] ;
2026-01-16 02:30:44 +01:00
2026-01-16 21:59:39 +01:00
for ( const ts of timestamps ) {
const start = new Date ( Number ( ts ) ) ;
const end = new Date ( start . getTime ( ) + 2 * 60 * 60 * 1000 ) ;
if ( end < now ) continue ;
2026-01-16 02:30:44 +01:00
2026-01-16 21:59:39 +01:00
const r = rotations [ ts ] ;
2026-01-16 02:30:44 +01:00
2026-01-22 23:08:26 -05:00
result . push ( {
turf : stageNames ( r . turfStages ) ,
ranked : stageNames ( r . rankedStages ) ,
ranked _mode : r . rankedMode . replace ( /([a-z])([A-Z])/g , "$1 $2" ) ,
startTime : start ,
endTime : end
} ) ;
2026-01-16 21:59:39 +01:00
}
2026-01-22 23:08:26 -05:00
return result ;
2026-01-16 21:59:39 +01:00
} catch ( error ) {
console . error ( "Failed to fetch rotations:" , error ) ;
return [ ] ;
2026-01-16 02:30:44 +01:00
}
2026-01-16 21:59:39 +01:00
}
2026-01-16 02:30:44 +01:00
2026-01-16 21:59:39 +01:00
function animateLoadingCanvas ( ) {
const canvas = document . getElementById ( "loading-canvas" ) ;
const loadingOverlay = document . getElementById ( "loading-overlay" ) ;
if ( ! canvas || ! loadingOverlay ) return ;
const ctx = canvas . getContext ( "2d" ) ;
const image = new Image ( ) ;
image . src = "/assets/loading.png" ;
const frameWidth = 100 ;
const frameHeight = 100 ;
const totalFrames = 17 ;
let currentFrame = 0 ;
let lastUpdateTime = 0 ;
let animationId ;
image . onload = ( ) => {
function animate ( timestamp = 0 ) {
if ( loadingOverlay . style . display === "none" ) {
cancelAnimationFrame ( animationId ) ;
return ;
}
if ( ! lastUpdateTime ) lastUpdateTime = timestamp ;
const elapsed = timestamp - lastUpdateTime ;
2026-01-22 23:08:26 -05:00
if ( elapsed > 50 ) {
2026-01-16 21:59:39 +01:00
ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
ctx . drawImage (
image ,
2026-01-22 23:08:26 -05:00
0 ,
currentFrame * frameHeight ,
frameWidth ,
frameHeight ,
0 ,
0 ,
frameWidth ,
frameHeight
2026-01-16 21:59:39 +01:00
) ;
currentFrame = ( currentFrame + 1 ) % totalFrames ;
lastUpdateTime = timestamp ;
}
animationId = requestAnimationFrame ( animate ) ;
}
animationId = requestAnimationFrame ( animate ) ;
} ;
2026-01-16 02:30:44 +01:00
}
async function renderStages ( ) {
2026-01-16 21:59:39 +01:00
const loadingOverlay = document . getElementById ( "loading-overlay" ) ;
2026-01-16 02:30:44 +01:00
const container = document . getElementById ( "stages-container" ) ;
2026-01-16 21:59:39 +01:00
let rotations = [ ] ;
try {
rotations = await fetchRotations ( ) ;
} catch ( err ) {
console . error ( "Error in renderStages:" , err ) ;
} finally {
2026-01-22 23:08:26 -05:00
loadingOverlay . style . display = "none" ;
2026-01-16 21:59:39 +01:00
}
2026-01-16 02:30:44 +01:00
if ( ! rotations . length ) {
2026-01-22 23:08:26 -05:00
container . innerHTML = ` <h2 class="error-message">Something went wrong! Try reloading the page. If this problem persists you may be ratelimited.</h2> ` ;
2026-01-16 02:30:44 +01:00
return ;
}
for ( const rotation of rotations ) {
2026-01-22 23:08:26 -05:00
const optionsDate = { day : "2-digit" , month : "2-digit" } ;
const optionsTime = { hour : "numeric" , minute : "2-digit" , hour12 : true } ;
const timeZone = "Europe/Paris" ;
const formattedTime = ` ${ rotation . startTime . toLocaleDateString ( "en-GB" , { ... optionsDate , timeZone } )} at ${ rotation . startTime . toLocaleTimeString ( "en-GB" , { ... optionsTime , timeZone } )} (CEST) ~ ${ rotation . endTime . toLocaleDateString ( "en-GB" , { ... optionsDate , timeZone } )} at ${ rotation . endTime . toLocaleTimeString ( "en-GB" , { ... optionsTime , timeZone } )} (CEST) ` ;
2026-01-16 02:30:44 +01:00
container . innerHTML += `
2026-01-22 23:08:26 -05:00
< div class = "rotation-time" > $ { formattedTime } < / d i v >
2026-01-16 02:30:44 +01:00
< div class = "stages-section" >
2026-01-22 23:08:26 -05:00
< img class = "mode-title" src = "/assets/regular.svg" alt = "Regular Battle" >
< div class = "mode-text" > Regular Battle < / d i v >
2026-01-16 02:30:44 +01:00
< div class = "stages" >
2026-01-22 23:08:26 -05:00
$ { rotation . turf . map ( name => `
< div class = "stage-tile" >
< img src = "${stageImagePath(name)}" alt = "${name}" >
< div class = "stage-title" > $ { name } < / d i v >
< / d i v >
` ).join("")}
2026-01-16 02:30:44 +01:00
< / d i v >
< / d i v >
< div class = "stages-section" >
2026-01-22 23:08:26 -05:00
< img class = "mode-title" src = "/assets/ranked.svg" alt = "Ranked Battle" >
< div class = "mode-text" > Ranked Battle < / d i v >
< div class = "ranked-mode-labels" >
< div class = "battle-mode-text" > Battle Mode < / d i v >
< div class = "ranked-mode-text" > $ { rotation . ranked _mode } < / d i v >
< / d i v >
2026-01-16 02:30:44 +01:00
< div class = "stages" >
2026-01-22 23:08:26 -05:00
$ { rotation . ranked . map ( name => `
< div class = "stage-tile" >
< img src = "${stageImagePath(name)}" alt = "${name}" >
< div class = "stage-title" > $ { name } < / d i v >
< / d i v >
` ).join("")}
2026-01-16 02:30:44 +01:00
< / d i v >
< / d i v >
` ;
}
}
2026-01-17 00:12:37 +01:00
animateLoadingCanvas ( ) ;
2026-01-16 02:30:44 +01:00
renderStages ( ) ;