diff --git a/static/js/task.js b/static/js/task.js index 3c40c75..c7f27f3 100644 --- a/static/js/task.js +++ b/static/js/task.js @@ -1,61 +1,239 @@ -$(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 grind!'; + confirmButton.disabled = true; + confirmButton.textContent = 'Rolling...'; + + const setVisualTask = (task, index, total) => { + if (!task) { + return; + } + 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}%`; + 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'; +} + +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(){ - 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 = resolveTaskImageSrc(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 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 +247,58 @@ $(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; + const imageSrc = resolveTaskImageSrc(taskData.image); + image.src = imageSrc; + imagePreview.src = imageSrc; + }; + 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 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 %}