From a4e21111f8b086f424962563f67af87036e70a91 Mon Sep 17 00:00:00 2001 From: Gerni <47844854+mgerni@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:23:58 -0500 Subject: [PATCH 1/5] updated rolling animation --- static/js/task.js | 362 ++++++++++++++++++++++-------- static/styles.css | 82 +++++++ task_database.py | 21 ++ taskapp.py | 21 +- templates/dashboard_official.html | 147 +----------- 5 files changed, 390 insertions(+), 243 deletions(-) diff --git a/static/js/task.js b/static/js/task.js index 3c40c75..bc5f646 100644 --- a/static/js/task.js +++ b/static/js/task.js @@ -1,61 +1,229 @@ -$(window).on('load', function(){ - var frameSpeed = 1000, - frameContainer = $('#frame-container'), - frames = $('.frame',frameContainer ), - frameCount = frames.length, - messageContainer = $('#message-container'), - messages = $('.message', messageContainer) - messageCount = messages.length, - t = null, - start = $('#start'), - showFrame = function (n){ - if (n != frameCount){ - return frames.hide().eq(n).show() && messages.hide().eq(n).show(); - - } - return frames.eq(frameCount).show() && messages.eq(messageCount).show(); - - }, - nextFrame = function(){ - if (index == frameCount){ - stopFrames(); - showFrame(frameCount - 1); - } - else { - showFrame(++index); - t = setTimeout(nextFrame,frameSpeed); - } - - }, - stopFrames = function(){ - clearInterval(t); - index = 0; - }; - frameContainer - start.on('click', nextFrame) - stopFrames(); - showFrame(0); -}); +function uniqueRollPool(tasks) { + const seen = new Set(); + const output = []; + (tasks || []).forEach((task) => { + if (!task || !task.name || !task.image) { + return; + } + const key = `${task.name}|${task.image}|${task.link || ''}`; + if (!seen.has(key)) { + seen.add(key); + output.push(task); + } + }); + return output; +} + +function shufflePool(tasks) { + const shuffled = [...tasks]; + for (let i = shuffled.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + return shuffled; +} + +function buildRollSequence(pool, finalTask) { + const safePool = uniqueRollPool(pool); + if (!safePool.length) { + return [finalTask, finalTask, finalTask, finalTask, finalTask].filter(Boolean); + } + + const sequence = []; + const spins = Math.max(10, Math.floor(safePool.length * 1.35)); + while (sequence.length < spins) { + const chunk = shufflePool(safePool); + for (let i = 0; i < chunk.length && sequence.length < spins; i += 1) { + sequence.push(chunk[i]); + } + } + sequence.push(finalTask); + return sequence; +} + +function runSlotRollAnimation(sequence, renderTask, onComplete) { + let index = 0; + + const tick = () => { + renderTask(sequence[index], index, sequence.length); + index += 1; + + if (index >= sequence.length) { + if (typeof onComplete === 'function') { + onComplete(sequence[sequence.length - 1]); + } + return; + } + + const progress = index / sequence.length; + const delay = 30 + Math.floor(Math.pow(progress, 2) * 150); + window.setTimeout(tick, delay); + }; + + tick(); +} + +function ensureTaskRollModal() { + let dialog = document.getElementById('taskRollModal'); + if (dialog) { + return dialog; + } + + dialog = document.createElement('dialog'); + dialog.id = 'taskRollModal'; + dialog.className = 'task-action-modal task-roll-modal rsText'; + dialog.innerHTML = ` +
+
+

Rolling New Task

+
+
+

Welcome to your next grind!

+
+ Rolling task image +
+
Rolling...
+
+
+
+
+
+ +
+ `; + + document.body.appendChild(dialog); + return dialog; +} + +function showTaskRollModal(options) { + const { + title, + subtitle, + sequence, + onRender, + onComplete, + } = options; + + const modal = ensureTaskRollModal(); + const titleNode = modal.querySelector('#taskRollTitle'); + const subtitleNode = modal.querySelector('#taskRollSubtitle'); + const imageNode = modal.querySelector('#taskRollImage'); + const nameNode = modal.querySelector('#taskRollName'); + const progressFill = modal.querySelector('#taskRollProgressFill'); + const confirmButton = modal.querySelector('#taskRollConfirm'); + + titleNode.textContent = title || 'Rolling New Task'; + subtitleNode.textContent = subtitle || 'This could be your next 100-hour grind.'; + confirmButton.disabled = true; + confirmButton.textContent = 'Rolling...'; + + const setVisualTask = (task, index, total) => { + if (!task) { + return; + } + imageNode.src = task.image; + nameNode.textContent = task.name; + const progress = total > 1 ? Math.min(100, Math.floor((index / (total - 1)) * 100)) : 100; + progressFill.style.width = `${progress}%`; + if (typeof onRender === 'function') { + onRender(task); + } + }; + + confirmButton.onclick = () => { + if (typeof modal.close === 'function') { + modal.close(); + } + }; + + modal.addEventListener('cancel', (event) => { + if (confirmButton.disabled) { + event.preventDefault(); + } + }, { once: true }); + + if (typeof modal.showModal === 'function') { + modal.showModal(); + } + + runSlotRollAnimation(sequence, setVisualTask, (finalTask) => { + progressFill.style.width = '100%'; + confirmButton.disabled = false; + confirmButton.textContent = 'Begin Grind'; + if (typeof onComplete === 'function') { + onComplete(finalTask); + } + }); +} + +function loadAvailableRollTasks(tier) { + const payload = tier ? { tier: `${tier}Tasks` } : {}; + return $.ajax({ + url: '/available_roll_tasks/', + type: 'POST', + data: payload, + }); +} + +function getCurrentUsername() { + const profileLink = document.querySelector('a.profile'); + if (!profileLink) { + return ''; + } + return (profileLink.textContent || '').trim(); +} + +function isSpecialInstantRollUser() { + const username = getCurrentUsername().toLowerCase(); + return username === 'shadukat' || username === 'gerni shadu'; +} $(document).on('click', '#start', function(){ - req = $.ajax({ + const startButton = document.getElementById('start'); + const completeButton = document.getElementById('complete'); + startButton.disabled = true; + + const generateRequest = $.ajax({ url : '/generate/', type : 'POST' - }) - - req.done(function(data){ - delay(function(){ - const message = document.getElementById("message_target"); - const image = document.getElementById("image_target"); - const imageLink = document.getElementById("taskImage"); - imageLink.href = data.link; - imageLink.setAttribute('data-tip', data.tip); - message.innerHTML = data.name; - image.src = data.image; - document.getElementById("start").disabled = true; - document.getElementById("complete").disabled = false; - }, 6000); }); + const poolRequest = loadAvailableRollTasks(null); + + $.when(generateRequest, poolRequest) + .done(function(generateResponse, poolResponse){ + const generatedTask = generateResponse[0]; + const availableTasks = (poolResponse[0] && poolResponse[0].tasks) ? poolResponse[0].tasks : []; + + const renderTask = function(task) { + const message = document.getElementById('message_target'); + const image = document.getElementById('image_target'); + const imageLink = document.getElementById('taskImage'); + imageLink.href = task.link; + imageLink.setAttribute('data-tip', task.tip || ''); + message.innerHTML = task.name; + image.src = task.image; + }; + + const sequence = buildRollSequence(availableTasks, generatedTask); + const instantRoll = isSpecialInstantRollUser(); + showTaskRollModal({ + title: 'Official Task Roll', + subtitle: instantRoll ? 'Hi Youtube <3 Gerni Task' : 'This could be your next 100-hour grind.', + sequence: instantRoll ? [generatedTask] : sequence, + onRender: null, + onComplete: function() { + renderTask(generatedTask); + startButton.disabled = true; + completeButton.disabled = false; + } + }); + }) + .fail(function(){ + startButton.disabled = false; + }); }); $(document).on('click', '#complete', function(){ @@ -69,57 +237,57 @@ $(document).on('click', '#complete', function(){ }) }); - -var delay = (function(){ - var timer = 0; - return function(callback, ms) { - clearTimeout (timer); - timer = setTimeout(callback, ms); - }; -})(); - -// $(document).on('click', '#easy_generate', function(){ -// $('form').submit(false); -// req = $.ajax({ -// url : '/generate_unofficial_easy/', -// type : 'POST' -// }); -// req.done(function(data){ -// const task = document.getElementById("easy_task") -// const image = document.getElementById("easy_image") -// const imagePreview = document.getElementById("easy_image_preview") -// task.innerHTML = data.name -// image.src = "/static/assets/" + data.image -// imagePreview.src = "/static/assets/" + data.image - -// }); -// }); - $(document).on('click', '#generate_unofficial', function(){ $('form').submit(false); - let tier = this.name - req = $.ajax({ + let tier = this.name; + const generateButton = this; + generateButton.disabled = true; + + const generateRequest = $.ajax({ url : '/generate_unofficial/', type : 'POST', data : {tier : tier + 'Tasks'} }); - req.done(function(data){ - const task = document.getElementById(tier + "_task"); - const image = document.getElementById(tier + "_image"); - const imagePreview = document.getElementById(tier + "_image_preview"); - var imagePlaceholder = document.getElementById(tier + "_placeholder"); - if (!imagePlaceholder){ - imagePlaceholder = document.getElementById(tier + '_imageTask') - } - imagePlaceholder.setAttribute('data-tip', data.tip) - imagePlaceholder.href = data.link - imagePreview.name = data.name; - task.innerHTML = data.name; - image.src = data.image; - imagePreview.src = data.image; - }); -}); + const poolRequest = loadAvailableRollTasks(tier); + $.when(generateRequest, poolRequest) + .done(function(generateResponse, poolResponse){ + const generatedTask = generateResponse[0]; + const availableTasks = (poolResponse[0] && poolResponse[0].tasks) ? poolResponse[0].tasks : []; + + const renderTask = function(taskData) { + const task = document.getElementById(tier + '_task'); + const image = document.getElementById(tier + '_image'); + const imagePreview = document.getElementById(tier + '_image_preview'); + let imagePlaceholder = document.getElementById(tier + '_placeholder'); + if (!imagePlaceholder){ + imagePlaceholder = document.getElementById(tier + '_imageTask'); + } + imagePlaceholder.setAttribute('data-tip', taskData.tip || ''); + imagePlaceholder.href = taskData.link || '#'; + imagePreview.name = taskData.name; + task.innerHTML = taskData.name; + image.src = taskData.image; + imagePreview.src = taskData.image; + }; + + const sequence = buildRollSequence(availableTasks, generatedTask); + const instantRoll = isSpecialInstantRollUser(); + showTaskRollModal({ + title: `${tier.charAt(0).toUpperCase() + tier.slice(1)} Task Roll`, + subtitle: instantRoll ? 'Hi Youtube <3 Gerni Task' : 'This could be your next 100-hour grind.', + sequence: instantRoll ? [generatedTask] : sequence, + onRender: null, + onComplete: function() { + renderTask(generatedTask); + generateButton.disabled = false; + } + }); + }) + .fail(function(){ + generateButton.disabled = false; + }); +}); $(document).on('click', '#complete_unofficial', function(){ $('form').submit(false); diff --git a/static/styles.css b/static/styles.css index 903ae91..c9d1084 100644 --- a/static/styles.css +++ b/static/styles.css @@ -691,6 +691,88 @@ hr { justify-content: center; } +.task-roll-modal { + max-width: calc(100vw - 20px); +} + +.task-roll-shell { + min-width: 420px; + max-width: 620px; + width: max-content; + min-height: 420px; +} + +.task-roll-header { + text-align: center; + justify-content: center; +} + +.task-roll-title { + margin: 0; +} + +.task-roll-divider { + margin: 8px 0; +} + +.task-roll-subtitle { + margin: 0 0 10px 0; + text-align: center; + color: var(--current-task-color); +} + +.task-roll-reel { + display: flex; + justify-content: center; + align-items: center; + min-height: 170px; + border: 1px solid #606060; + background: #3f3930; +} + +.task-roll-image { + width: 96px; + height: 96px; + object-fit: contain; + image-rendering: pixelated; +} + +.task-roll-name { + margin: 10px 0 8px 0; + text-align: center; + font-size: 1.05rem; + min-height: 26px; +} + +.task-roll-progress-wrap { + margin-bottom: 12px; +} + +.task-roll-progress-bar { + height: 22px; +} + +.task-roll-progress-fill { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0%; + background: linear-gradient(90deg, #4f6f44 0%, #6d8d5f 100%); +} + +.task-roll-confirm { + min-width: 180px; +} + +@media (max-width: 640px) { + .task-roll-shell { + min-width: 320px; + max-width: calc(100vw - 20px); + width: calc(100vw - 20px); + } +} + @media (max-width: 640px) { .task-action-shell { min-width: 320px; diff --git a/task_database.py b/task_database.py index 361381a..01bec78 100644 --- a/task_database.py +++ b/task_database.py @@ -376,6 +376,27 @@ def generate_task_for_tier(username, tier) -> TaskData or None: # type: ignore __set_current_task(username, task_tier, task_number, False) return None + +def get_available_roll_tasks(username: str, tier: str | None = None) -> tuple[str | None, list[TaskData]]: + user = get_user(username) + + def get_uncompleted_for_tier(target_tier: str) -> list[TaskData]: + normalized_tier = target_tier.replace('Tasks', '') + all_tasks = tasklists.list_for_tier(normalized_tier, user.lms_enabled) + completed_task_ids = {task.id for task in user.get_task_list(normalized_tier).completed_tasks} + return [task for task in all_tasks if task.id not in completed_task_ids] + + if tier: + normalized_tier = tier.replace('Tasks', '') + return normalized_tier, get_uncompleted_for_tier(normalized_tier) + + for candidate_tier in ['easy', 'medium', 'hard', 'elite', 'master']: + available = get_uncompleted_for_tier(candidate_tier) + if available: + return candidate_tier, available + + return None, [] + ''' generate_task: diff --git a/taskapp.py b/taskapp.py index e268ff0..51febd1 100644 --- a/taskapp.py +++ b/taskapp.py @@ -12,7 +12,8 @@ manual_complete_tasks, manual_revert_tasks, get_lms_status, lms_status_change, update_imported_tasks, official_status_change, username_change, get_taskCurrent_tier, generate_task_for_tier, - complete_task_unofficial_tier, get_user, get_leaderboard) + complete_task_unofficial_tier, get_user, get_leaderboard, + get_available_roll_tasks) import send_grid_email from templesync import check_logs @@ -535,6 +536,24 @@ def generate_button(): data = {"name" : task.name, "image" : task.image_link, "tip" : task.tip, "link" : task.wiki_link} return data + +@app.route('/available_roll_tasks/', methods=['POST']) +@login_required +def available_roll_tasks(): + tier = request.form.get('tier') + _, tasks = get_available_roll_tasks(session['username'], tier) + return { + 'tasks': [ + { + 'name': task.name, + 'image': task.image_link, + 'tip': task.tip, + 'link': task.wiki_link, + } + for task in tasks + ] + } + @app.route('/complete/', methods =['POST']) @login_required def complete_button(): diff --git a/templates/dashboard_official.html b/templates/dashboard_official.html index 68d3e20..3a508ab 100644 --- a/templates/dashboard_official.html +++ b/templates/dashboard_official.html @@ -29,23 +29,11 @@

Current Task

{% else %}
- - - - - - - +
-

You have no task!

-

So many tasks

-

So little time

-

What will be

-

Your next grind?

-

Easy or Hard

-

It's time to GRIND!

+

You have no task!

{% endif %} @@ -63,135 +51,4 @@

Current Task

{% endif %} - - - - - {% endblock %} From 48b4b9f939a2900518d0e18782d4247bdd49ef59 Mon Sep 17 00:00:00 2001 From: Gerni <47844854+mgerni@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:33:16 -0500 Subject: [PATCH 2/5] fix merge conflict for roll animation --- static/js/task.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/static/js/task.js b/static/js/task.js index bc5f646..cf27031 100644 --- a/static/js/task.js +++ b/static/js/task.js @@ -124,7 +124,7 @@ function showTaskRollModal(options) { if (!task) { return; } - imageNode.src = task.image; + imageNode.src = resolveTaskImageSrc(task.image); nameNode.textContent = task.name; const progress = total > 1 ? Math.min(100, Math.floor((index / (total - 1)) * 100)) : 100; progressFill.style.width = `${progress}%`; @@ -181,6 +181,16 @@ function isSpecialInstantRollUser() { return username === 'shadukat' || username === 'gerni shadu'; } +function resolveTaskImageSrc(imageValue) { + if (!imageValue || typeof imageValue !== 'string') { + return '/static/assets/Cake_of_guidance_detail.png'; + } + if (imageValue.startsWith('/static/') || imageValue.startsWith('http://') || imageValue.startsWith('https://')) { + return imageValue; + } + return `/static/assets/${imageValue}`; +} + $(document).on('click', '#start', function(){ const startButton = document.getElementById('start'); const completeButton = document.getElementById('complete'); @@ -204,7 +214,7 @@ $(document).on('click', '#start', function(){ imageLink.href = task.link; imageLink.setAttribute('data-tip', task.tip || ''); message.innerHTML = task.name; - image.src = task.image; + image.src = resolveTaskImageSrc(task.image); }; const sequence = buildRollSequence(availableTasks, generatedTask); @@ -267,8 +277,9 @@ $(document).on('click', '#generate_unofficial', function(){ imagePlaceholder.href = taskData.link || '#'; imagePreview.name = taskData.name; task.innerHTML = taskData.name; - image.src = taskData.image; - imagePreview.src = taskData.image; + const imageSrc = resolveTaskImageSrc(taskData.image); + image.src = imageSrc; + imagePreview.src = imageSrc; }; const sequence = buildRollSequence(availableTasks, generatedTask); From 85646b5a32eb46934c0d8b5556614aa8db7e07e4 Mon Sep 17 00:00:00 2001 From: Gerni <47844854+mgerni@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:37:13 -0500 Subject: [PATCH 3/5] updated titles --- static/js/task.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/task.js b/static/js/task.js index cf27031..eddc978 100644 --- a/static/js/task.js +++ b/static/js/task.js @@ -116,7 +116,7 @@ function showTaskRollModal(options) { const confirmButton = modal.querySelector('#taskRollConfirm'); titleNode.textContent = title || 'Rolling New Task'; - subtitleNode.textContent = subtitle || 'This could be your next 100-hour grind.'; + subtitleNode.textContent = subtitle || 'This could be your next grind.'; confirmButton.disabled = true; confirmButton.textContent = 'Rolling...'; @@ -221,7 +221,7 @@ $(document).on('click', '#start', function(){ const instantRoll = isSpecialInstantRollUser(); showTaskRollModal({ title: 'Official Task Roll', - subtitle: instantRoll ? 'Hi Youtube <3 Gerni Task' : 'This could be your next 100-hour grind.', + subtitle: instantRoll ? 'Hi Youtube <3 Gerni Task' : 'This could be your next grind.', sequence: instantRoll ? [generatedTask] : sequence, onRender: null, onComplete: function() { @@ -286,7 +286,7 @@ $(document).on('click', '#generate_unofficial', function(){ const instantRoll = isSpecialInstantRollUser(); showTaskRollModal({ title: `${tier.charAt(0).toUpperCase() + tier.slice(1)} Task Roll`, - subtitle: instantRoll ? 'Hi Youtube <3 Gerni Task' : 'This could be your next 100-hour grind.', + subtitle: instantRoll ? 'Hi Youtube <3 Gerni Task' : 'This could be your next grind.', sequence: instantRoll ? [generatedTask] : sequence, onRender: null, onComplete: function() { From c23fa9c43745ddf4deb3f59cbbbed8234b478d8b Mon Sep 17 00:00:00 2001 From: Gerni <47844854+mgerni@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:40:45 -0500 Subject: [PATCH 4/5] fix all the things --- static/js/task.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/task.js b/static/js/task.js index eddc978..c7f27f3 100644 --- a/static/js/task.js +++ b/static/js/task.js @@ -116,7 +116,7 @@ function showTaskRollModal(options) { const confirmButton = modal.querySelector('#taskRollConfirm'); titleNode.textContent = title || 'Rolling New Task'; - subtitleNode.textContent = subtitle || 'This could be your next grind.'; + subtitleNode.textContent = subtitle || 'This could be your next grind!'; confirmButton.disabled = true; confirmButton.textContent = 'Rolling...'; From 3ed1728bc02530accf507de839c18b8f4f8783d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 00:43:38 +0000 Subject: [PATCH 5/5] Initial plan