Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
373 changes: 276 additions & 97 deletions static/js/task.js
Original file line number Diff line number Diff line change
@@ -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);
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the final task is null or undefined (if generation fails), this will create a sequence ending with null. The subsequent code in showTaskRollModal may fail when trying to access properties on the finalTask. Consider adding validation to handle cases where finalTask might be null/undefined, or ensure the calling code validates this before building the sequence.

Suggested change
sequence.push(finalTask);
if (finalTask) {
sequence.push(finalTask);
}

Copilot uses AI. Check for mistakes.
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 = `
<div class="rect task-action-shell task-roll-shell">
<div class="task-action-header task-roll-header">
<h3 id="taskRollTitle" class="task-roll-title">Rolling New Task</h3>
</div>
<hr class="task-action-divider task-roll-divider">
<p id="taskRollSubtitle" class="task-roll-subtitle">Welcome to your next grind!</p>
<div class="task-roll-reel">
<img id="taskRollImage" class="task-roll-image" src="/static/assets/Cake_of_guidance_detail.png" alt="Rolling task image" />
</div>
<div id="taskRollName" class="task-roll-name">Rolling...</div>
<div class="task-action-progress-wrap task-roll-progress-wrap">
<div class="task-action-progress-bar task-roll-progress-bar">
<div id="taskRollProgressFill" class="task-roll-progress-fill"></div>
</div>
</div>
<div class="task-action-footer">
<button id="taskRollConfirm" class="button-style task-roll-confirm" type="button" disabled>Rolling...</button>
</div>
</div>
`;

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 });
Comment on lines +142 to +146
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'cancel' event listener is added with { once: true } each time showTaskRollModal is called. If the modal is reused multiple times without being recreated (which ensureTaskRollModal does), subsequent calls won't register new event listeners since the previous ones are removed after firing once. This could lead to inconsistent behavior. Consider either always recreating the modal, or managing event listeners more carefully to ensure they're properly attached for each modal display.

Suggested change
modal.addEventListener('cancel', (event) => {
if (confirmButton.disabled) {
event.preventDefault();
}
}, { once: true });
// Ensure we don't accumulate multiple cancel listeners across calls.
if (modal._taskRollCancelHandler) {
modal.removeEventListener('cancel', modal._taskRollCancelHandler);
}
const cancelHandler = (event) => {
if (confirmButton.disabled) {
event.preventDefault();
}
};
modal._taskRollCancelHandler = cancelHandler;
modal.addEventListener('cancel', cancelHandler);

Copilot uses AI. Check for mistakes.

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';
Comment on lines +179 to +181
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function hardcodes specific usernames ('shadukat' and 'gerni shadu') to provide special behavior. Hardcoded usernames in business logic create maintainability concerns and make the code less flexible. Consider moving these special user identifiers to a configuration file or database, or implementing a user role/flag system that can be managed through the admin interface rather than requiring code changes.

Suggested change
function isSpecialInstantRollUser() {
const username = getCurrentUsername().toLowerCase();
return username === 'shadukat' || username === 'gerni shadu';
function getSpecialInstantRollUsers() {
if (typeof window !== 'undefined' &&
Array.isArray(window.SPECIAL_INSTANT_ROLL_USERS) &&
window.SPECIAL_INSTANT_ROLL_USERS.length > 0) {
return window.SPECIAL_INSTANT_ROLL_USERS
.map((name) => (typeof name === 'string' ? name.toLowerCase() : ''))
.filter(Boolean);
}
// Default special users for instant roll behavior; can be overridden
// by defining window.SPECIAL_INSTANT_ROLL_USERS elsewhere.
return ['shadukat', 'gerni shadu'];
}
function isSpecialInstantRollUser() {
const username = getCurrentUsername().toLowerCase();
if (!username) {
return false;
}
const specialUsers = getSpecialInstantRollUsers();
return specialUsers.indexOf(username) !== -1;

Copilot uses AI. Check for mistakes.
}

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(){
Expand All @@ -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);
Expand Down
Loading