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 = `
+
+
+
+
Welcome to your next grind!
+
+

+
+
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 %}