window.addEventListener("DOMContentLoaded", () => { //Create progress bar const book_bar = new ProgressBar(); //Pat Lilac const websocket = new ReconnectingWebSocket("wss://lilac.thewisewolf.dev/campaigns/partnerplus2026/ws"); communicate(websocket, book_bar); }); class ProgressBar { constructor() { this.current_progress = 25; this.max_progress = 45; this.anim_progress = 0; this.animating = false; this.simming = false; this.target_element = document.getElementById("progressBar"); this.bar_back = document.getElementById("barBack"); this.bar_front = document.getElementById("barFront"); //Pre-load and hold background images let image_cache = []; for (let i = 0; i < 46; i++) { const img_fetcher = new Image(); let index_string = i.toString(); if (index_string.length < 2) { index_string = "0" + index_string; } img_fetcher.src = "src/Books/BookBar_v8_f00" + index_string + ".png"; image_cache.push(img_fetcher); }; } //SET NUMBER OF COLORED BOOKS //Set current campaign progress setProgress(val) { if (Number.isInteger(val) && (val >= 0) && (val <= this.max_progress)) { this.current_progress = val; } else { console.warn("Progress update failed.") } } //Render current animation state to the screen renderProgress(val) { if (Number.isInteger(val) && (this.target_element != null) && (this.bar_back != null) && (this.bar_front != null) && (val >= 0) && (val <= this.current_progress) && (val <= this.max_progress)) { let val_string = val.toString(); if (val_string.length < 2) { val_string = "0" + val_string; } this.bar_back.style.backgroundImage = "url(src/Books/BookBar_v8_f00" + val_string + ".png)"; setTimeout(this.antiFlash.bind(this), 150, val_string, val); } else { console.warn("Progress render failed.") } } //Prevent image flicker by pre-loading a version of the image then rendering the top image with a delay antiFlash(val_string, new_val) { this.bar_front.style.backgroundImage = "url(src/Books/BookBar_v8_f00" + val_string + ".png)"; //Increment the animation ticker (plus an extra round of safety checks) if ((new_val >= 0) && (new_val <= this.current_progress) && (this.anim_progress < this.max_progress) && (new_val < this.max_progress)) { this.anim_progress = new_val; } else { console.warn("Anim update is out of range."); } } //Progress Animation animProgress() { //animate if (this.animating == true) { if ((this.anim_progress >= 0) && (this.anim_progress < this.current_progress) && (this.anim_progress < this.max_progress)) { this.renderProgress(this.anim_progress + 1); setTimeout(this.animProgress.bind(this), 500); } //loop if (this.anim_progress == this.current_progress) { setTimeout(() => this.renderProgress(0), 5000); setTimeout(this.animProgress.bind(this), 5500); } } } //Toggle animation on animStart() { if (this.animating != true){ this.animating = true; this.animProgress(); } else { console.warn("Animation already running"); } } //Toggle animation off animEnd() { this.animating = false; } //TEST ANIMATION simProgress() { //sim if (this.simming == true) { if (this.current_progress < this.max_progress) { this.setProgress(this.current_progress + 1); setTimeout(this.simProgress.bind(this), 38500); } //loop if (this.current_progress == this.max_progress) { setTimeout(() => this.setProgress(0), 10000); setTimeout(this.simProgress.bind(this), 95500); } } } simStart() { console.log("starting"); this.simming = true; this.simProgress(); console.log("started"); } simEnd() { console.log("ending"); this.simming = false; } } class ReconnectingWebSocket { constructor(url) { this.url = url; this.ws = null; this.attempt = 0; this.connect(); this.closing = false; this.onopen = null; this.onmessage = null; this.onclose = null; this.onerror = null; } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('WebSocket connecting'); this.attempt = 0; if (this.onopen != null) { this.onopen(); } }; this.ws.onmessage = (msg) => { console.log('Received:', msg.data); if (this.onmessage != null) { this.onmessage(msg); } }; this.ws.onclose = () => { if (this.closing) { console.log('Closing Websocket.') } { console.warn('WebSocket closed. Reconnecting... (disabled for now)'); this.reconnect(); } }; this.ws.onerror = (err) => { console.error('WebSocket error:', err); if (this.onerror != null) { this.onerror(); } this.close(); }; } reconnect() { const delay = this.getBackoffDelay(this.attempt); console.log(`Reconnecting in ${delay}ms`); setTimeout(() => { this.attempt++; this.connect(); }, delay); } close() { this.closing = true; this.ws.close(); if (this.onclose != null) { this.onclose(); } } getBackoffDelay(attempt) { const base = 500; // 0.5 second const max = 30000; // 30 seconds const jitter = Math.random() * 1000; return Math.min(base * 2 ** attempt + jitter, max); } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } else { console.warn('WebSocket not connected'); } } } function communicate(websocket, book_bar) { console.log("Communicating"); const params = new URLSearchParams(window.location.search); // INITIALIZE CLASS HERE book_bar.animStart(); //setTimeout(() => book_bar.animEnd(), 30000); //book_bar.simStart(); websocket.onopen = () => { websocket.send( JSON.stringify( { type: "init", channel: "SubTimer", community: params.get('community') } ) ); }; websocket.onmessage = ({ data }) => { console.log("messaged"); const event = JSON.parse(data); switch (event.type) { case "DO": let args = event.args; // Call the specified method switch (event.method) { case "setTimer": //do something with the new data console.log("set timer: " + event); book_bar.setProgress(event.reward_progress); break; case "noTimer": //do something default console.log("no timer: " + event); break; case "endTimer": //do something for the final time console.log("end timer: " + event); break default: throw new Error(`Unsupported method requested: ${event.method}.`) } break; default: throw new Error(`Unsupported event type: ${event.type}.`); } }; }