import { fetchList } from "../content.js"; import { getThumbnailFromId, getYoutubeIdFromUrl, shuffle } from "../util.js"; import Spinner from "../components/Spinner.js"; import Btn from "../components/Btn.js"; export default { components: { Spinner, Btn }, template: ` Demon Roulette Based on the Extreme Demon Roulette by matcool. Progress {{ progression.length }} / {{ levels.length }} Highest % {{ currentPercentage }}% Streak 🔥 {{ streak }} levels Include Main List #1–75 Extended List #76–150 Legacy List #151+ {{ levels.length === 0 ? 'Start Roulette' : 'Restart' }} Save / Load Progress saves automatically. Import Export 🎲 Your first level is waiting… ▶ #{{ level.rank }} {{ level.name }} {{ progression[i] }}% ✓ ▶ #{{ currentLevel.rank }} {{ currentLevel.name }} {{ currentLevel.id }} Current Done Give Up {{ hasCompleted ? '🏆' : '💀' }} {{ hasCompleted ? 'Completed!' : 'Game Over' }} {{ progression.length }} Levels {{ currentPercentage }}% Best % {{ streak }} Peak Streak Show remaining levels ▶ #{{ level.rank }} {{ level.name }} Would need {{ currentPercentage + 2 + i }}% 🎮 Pick your lists and hit Start to begin. ⚠️ {{ toast }} `, data: () => ({ loading: false, levels: [], progression: [], percentage: undefined, givenUp: false, showRemaining: false, useMainList: true, useExtendedList: true, useLegacyList: false, toasts: [], fileInput: undefined, peakStreak: 0, }), mounted() { this.fileInput = document.createElement("input"); this.fileInput.type = "file"; this.fileInput.multiple = false; this.fileInput.accept = ".json"; this.fileInput.addEventListener("change", this.onImportUpload); const roulette = JSON.parse(localStorage.getItem("roulette")); if (!roulette) return; this.levels = roulette.levels; this.progression = roulette.progression; this.peakStreak = roulette.peakStreak || 0; }, computed: { currentLevel() { return this.levels[this.progression.length]; }, currentPercentage() { return this.progression[this.progression.length - 1] || 0; }, placeholder() { return `At least ${this.currentPercentage + 1}%`; }, hasCompleted() { return ( this.progression[this.progression.length - 1] >= 100 || this.progression.length === this.levels.length ); }, isActive() { return ( this.progression.length > 0 && !this.givenUp && !this.hasCompleted ); }, // 0–100 based on highest % reached, not levels completed progressPct() { return this.currentPercentage; }, // current consecutive streak of completed levels streak() { if (this.givenUp || this.hasCompleted) return this.peakStreak; return this.progression.length; }, }, methods: { shuffle, getThumbnailFromId, getYoutubeIdFromUrl, async onStart() { if (this.isActive) { this.showToast("Give up before starting a new roulette."); return; } if (!this.useMainList && !this.useExtendedList && !this.useLegacyList) return; this.loading = true; const fullListWithUnverifiedLevels = await fetchList(); const fullList = fullListWithUnverifiedLevels.filter(([_, rank]) => rank !== null); if (fullList.filter(([err]) => err).length > 0) { this.loading = false; this.showToast("List is currently broken. Wait until it's fixed to start a roulette."); return; } const fullListMapped = fullList.map(([_, rank, lvl], i) => ({ rank: i + 1, id: lvl.id, name: lvl.name, video: lvl.verification, })); const list = []; if (this.useMainList) list.push(...fullListMapped.slice(0, 75)); if (this.useExtendedList) list.push(...fullListMapped.slice(75, 150)); if (this.useLegacyList) list.push(...fullListMapped.slice(150)); this.levels = shuffle(list).slice(0, 100); this.showRemaining = false; this.givenUp = false; this.progression = []; this.percentage = undefined; this.peakStreak = 0; this.loading = false; }, save() { localStorage.setItem("roulette", JSON.stringify({ levels: this.levels, progression: this.progression, peakStreak: this.peakStreak, })); }, onDone() { if (!this.percentage) return; if (this.percentage <= this.currentPercentage || this.percentage > 100) { this.showToast("Invalid percentage."); return; } this.progression.push(this.percentage); this.peakStreak = Math.max(this.peakStreak, this.progression.length); this.percentage = undefined; this.save(); }, onGiveUp() { this.givenUp = true; localStorage.removeItem("roulette"); }, onImport() { if (this.isActive && !window.confirm("This will overwrite the currently running roulette. Continue?")) return; this.fileInput.showPicker(); }, async onImportUpload() { if (this.fileInput.files.length === 0) return; const file = this.fileInput.files[0]; if (file.type !== "application/json") { this.showToast("Invalid file."); return; } try { const roulette = JSON.parse(await file.text()); if (!roulette.levels || !roulette.progression) { this.showToast("Invalid file."); return; } this.levels = roulette.levels; this.progression = roulette.progression; this.peakStreak = roulette.peakStreak || 0; this.save(); this.givenUp = false; this.showRemaining = false; this.percentage = undefined; } catch { this.showToast("Invalid file."); } }, onExport() { const file = new Blob([JSON.stringify({ levels: this.levels, progression: this.progression, peakStreak: this.peakStreak })], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); a.download = "tsl_roulette"; a.click(); URL.revokeObjectURL(a.href); }, showToast(msg) { this.toasts.push(msg); setTimeout(() => this.toasts.shift(), 3000); }, }, };
Based on the Extreme Demon Roulette by matcool.
Include
Save / Load
Progress saves automatically.
🎲
Your first level is waiting…
🎮
Pick your lists and hit Start to begin.
{{ toast }}