/** @type {CharData} */ let characterData = []; // Initial character data set used. /** @type {CharData} */ let characterDataToSort = []; // Character data set after filtering. /** @type {Options} */ let options = []; // Initial option set used. let currentVersion = ''; // Which version of characterData and options are used. /** @type {(boolean|boolean[])[]} */ let optTaken = []; // Records which options are set. /** Save Data. Concatenated into array, joined into string (delimited by '|') and compressed with lz-string. */ let timestamp = 0; // savedata[0] (Unix time when sorter was started, used as initial PRNG seed and in dataset selection) let timeTaken = 0; // savedata[1] (Number of ms elapsed when sorter ends, used as end-of-sort flag and in filename generation) let choices = ''; // savedata[2] (String of '0', '1' and '2' that records what sorter choices are made) let optStr = ''; // savedata[3] (String of '0' and '1' that denotes top-level option selection) let suboptStr = ''; // savedata[4...n] (String of '0' and '1' that denotes nested option selection, separated by '|') let timeError = false; // Shifts entire savedata array to the right by 1 and adds an empty element at savedata[0] if true. /** Intermediate sorter data. */ let sortedIndexList = []; let recordDataList = []; let parentIndexList = []; let tiedDataList = []; let leftIndex = 0; let leftInnerIndex = 0; let rightIndex = 0; let rightInnerIndex = 0; let battleNo = 1; let sortedNo = 0; let pointer = 0; /** A copy of intermediate sorter data is recorded for undo() purposes. */ let sortedIndexListPrev = []; let recordDataListPrev = []; let parentIndexListPrev = []; let tiedDataListPrev = []; let leftIndexPrev = 0; let leftInnerIndexPrev = 0; let rightIndexPrev = 0; let rightInnerIndexPrev = 0; let battleNoPrev = 1; let sortedNoPrev = 0; let pointerPrev = 0; /** Miscellaneous sorter data that doesn't need to be saved for undo(). */ let finalCharacters = []; let loading = false; let totalBattles = 0; let sorterURL = window.location.host + window.location.pathname; let storedSaveType = localStorage.getItem(`${sorterURL}_saveType`); /** Initialize script. */ function init() { /** Define button behavior. */ document.querySelector('.starting.start.button').addEventListener('click', start); document.querySelector('.starting.load.button').addEventListener('click', loadProgress); document.querySelector('.left.sort.image').addEventListener('click', () => pick('left')); document.querySelector('.right.sort.image').addEventListener('click', () => pick('right')); document.querySelector('.sorting.tie.button').addEventListener('click', () => pick('tie')); document.querySelector('.sorting.undo.button').addEventListener('click', undo); document.querySelector('.sorting.save.button').addEventListener('click', () => saveProgress('Progress')); document.querySelector('.finished.save.button').addEventListener('click', () => saveProgress('Last Result')); document.querySelector('.finished.getimg.button').addEventListener('click', generateImage); document.querySelector('.finished.list.button').addEventListener('click', generateTextList); document.querySelector('.clearsave').addEventListener('click', clearProgress); /** Define keyboard controls (up/down/left/right vimlike k/j/h/l). */ document.addEventListener('keypress', (ev) => { /** If sorting is in progress. */ if (timestamp && !timeTaken && !loading && choices.length === battleNo - 1) { switch(ev.key) { case 's': case '3': saveProgress('Progress'); break; case 'h': case 'ArrowLeft': pick('left'); break; case 'l': case 'ArrowRight': pick('right'); break; case 'k': case '1': case 'ArrowUp': pick('tie'); break; case 'j': case '2': case 'ArrowDown': undo(); break; default: break; } } /** If sorting has ended. */ else if (timeTaken && choices.length === battleNo - 1) { switch(ev.key) { case 'k': case '1': saveProgress('Last Result'); break; case 'j': case '2': generateImage(); break; case 's': case '3': generateTextList(); break; default: break; } } else { // If sorting hasn't started yet. switch(ev.key) { case '1': case 's': case 'Enter': start(); break; case '2': case 'l': loadProgress(); break; default: break; } } }); document.querySelector('.image.selector').insertAdjacentElement('beforeend', document.createElement('select')); /** Initialize image quantity selector for results. */ for (let i = 0; i <= 10; i++) { const select = document.createElement('option'); select.value = i; select.text = i; if (i === 3) { select.selected = 'selected'; } document.querySelector('.image.selector > select').insertAdjacentElement('beforeend', select); } document.querySelector('.image.selector > select').addEventListener('input', (e) => { const imageNum = e.target.options[e.target.selectedIndex].value; result(Number(imageNum)); }); /** Show load button if save data exists. */ if (storedSaveType) { document.querySelector('.starting.load.button > span').insertAdjacentText('beforeend', storedSaveType); document.querySelectorAll('.starting.button').forEach(el => { el.style['grid-row'] = 'span 3'; el.style.display = 'block'; }); } setLatestDataset(); /** Decode query string if available. */ if (window.location.search.slice(1) !== '') decodeQuery(); } /** Begin sorting. */ function start() { /** Copy data into sorting array to filter. */ characterDataToSort = characterData.slice(0); /** Check selected options and convert to boolean array form. */ optTaken = []; options.forEach(opt => { if ('sub' in opt) { if (!document.getElementById(`cbgroup-${opt.key}`).checked) optTaken.push(false); else { const suboptArray = opt.sub.reduce((arr, val, idx) => { arr.push(document.getElementById(`cb-${opt.key}-${idx}`).checked); return arr; }, []); optTaken.push(suboptArray); } } else { optTaken.push(document.getElementById(`cb-${opt.key}`).checked); } }); /** Convert boolean array form to string form. */ optStr = ''; suboptStr = ''; optStr = optTaken .map(val => !!val) .reduce((str, val) => { str += val ? '1' : '0'; return str; }, optStr); optTaken.forEach(val => { if (Array.isArray(val)) { suboptStr += '|'; suboptStr += val.reduce((str, val) => { str += val ? '1' : '0'; return str; }, ''); } }); /** Filter out deselected nested criteria and remove selected criteria. */ options.forEach((opt, index) => { if ('sub' in opt) { if (optTaken[index]) { const subArray = optTaken[index].reduce((subList, subBool, subIndex) => { if (subBool) { subList.push(options[index].sub[subIndex].key); } return subList; }, []); characterDataToSort = characterDataToSort.filter(char => { if (!(opt.key in char.opts)) console.warn(`Warning: ${opt.key} not set for ${char.name}.`); return opt.key in char.opts && char.opts[opt.key].some(key => subArray.includes(key)); }); } } else if (optTaken[index]) { characterDataToSort = characterDataToSort.filter(char => !char.opts[opt.key]); } }); if (characterDataToSort.length < 2) { alert('Cannot sort with less than two characters. Please reselect.'); return; } /** Shuffle character array with timestamp seed. */ timestamp = timestamp || new Date().getTime(); if (new Date(timestamp) < new Date(currentVersion)) { timeError = true; } Math.seedrandom(timestamp); characterDataToSort = characterDataToSort .map(a => [Math.random(), a]) .sort((a,b) => a[0] - b[0]) .map(a => a[1]); /** * tiedDataList will keep a record of indexes on which characters are equal (i.e. tied) * to another one. recordDataList will have an interim list of sorted elements during * the mergesort process. */ recordDataList = characterDataToSort.map(() => 0); tiedDataList = characterDataToSort.map(() => -1); /** * Put a list of indexes that we'll be sorting into sortedIndexList. These will refer back * to characterDataToSort. * * Begin splitting each element into little arrays and spread them out over sortedIndexList * increasing its length until it become arrays of length 1 and you can't split it anymore. * * parentIndexList indicates each element's parent (i.e. where it was split from), except * for the first element, which has no parent. */ sortedIndexList[0] = characterDataToSort.map((val, idx) => idx); parentIndexList[0] = -1; let midpoint = 0; // Indicates where to split the array. let marker = 1; // Indicates where to place our newly split array. for (let i = 0; i < sortedIndexList.length; i++) { if (sortedIndexList[i].length > 1) { let parent = sortedIndexList[i]; midpoint = Math.ceil(parent.length / 2); sortedIndexList[marker] = parent.slice(0, midpoint); // Split the array in half, and put the left half into the marked index. totalBattles += sortedIndexList[marker].length; // The result's length will add to our total number of comparisons. parentIndexList[marker] = i; // Record where it came from. marker++; // Increment the marker to put the right half into. sortedIndexList[marker] = parent.slice(midpoint, parent.length); // Put the right half next to its left half. totalBattles += sortedIndexList[marker].length; // The result's length will add to our total number of comparisons. parentIndexList[marker] = i; // Record where it came from. marker++; // Rinse and repeat, until we get arrays of length 1. This is initialization of merge sort. } } leftIndex = sortedIndexList.length - 2; // Start with the second last value and... rightIndex = sortedIndexList.length - 1; // the last value in the sorted list and work our way down to index 0. leftInnerIndex = 0; // Inner indexes, because we'll be comparing the left array rightInnerIndex = 0; // to the right array, in order to merge them into one sorted array. /** Disable all checkboxes and hide/show appropriate parts while we preload the images. */ document.querySelectorAll('input[type=checkbox]').forEach(cb => cb.disabled = true); document.querySelectorAll('.starting.button').forEach(el => el.style.display = 'none'); document.querySelector('.loading.button').style.display = 'block'; document.querySelector('.progress').style.display = 'block'; loading = true; preloadImages().then(() => { loading = false; document.querySelector('.loading.button').style.display = 'none'; document.querySelectorAll('.sorting.button').forEach(el => el.style.display = 'block'); document.querySelectorAll('.sort.text').forEach(el => el.style.display = 'block'); display(); }); } /** Displays the current state of the sorter. */ function display() { const percent = Math.floor(sortedNo * 100 / totalBattles); const leftCharIndex = sortedIndexList[leftIndex][leftInnerIndex]; const rightCharIndex = sortedIndexList[rightIndex][rightInnerIndex]; const leftChar = characterDataToSort[leftCharIndex]; const rightChar = characterDataToSort[rightCharIndex]; const charNameDisp = name => { const charName = reduceTextWidth(name, 'Arial 12.8px', 220); const charTooltip = name !== charName ? name : ''; return `
${charName}
`; }; progressBar(`Battle No. ${battleNo}`, percent); document.querySelector('.left.sort.image').src = leftChar.img; document.querySelector('.right.sort.image').src = rightChar.img; document.querySelector('.left.sort.text').innerHTML = charNameDisp(leftChar.name); document.querySelector('.right.sort.text').innerHTML = charNameDisp(rightChar.name); /** Autopick if choice has been given. */ if (choices.length !== battleNo - 1) { switch (Number(choices[battleNo - 1])) { case 0: pick('left'); break; case 1: pick('right'); break; case 2: pick('tie'); break; default: break; } } else { saveProgress('Autosave'); } } /** * Sort between two character choices or tie. * * @param {'left'|'right'|'tie'} sortType */ function pick(sortType) { if ((timeTaken && choices.length === battleNo - 1) || loading) { return; } else if (!timestamp) { return start(); } sortedIndexListPrev = sortedIndexList.slice(0); recordDataListPrev = recordDataList.slice(0); parentIndexListPrev = parentIndexList.slice(0); tiedDataListPrev = tiedDataList.slice(0); leftIndexPrev = leftIndex; leftInnerIndexPrev = leftInnerIndex; rightIndexPrev = rightIndex; rightInnerIndexPrev = rightInnerIndex; battleNoPrev = battleNo; sortedNoPrev = sortedNo; pointerPrev = pointer; /** * For picking 'left' or 'right': * * Input the selected character's index into recordDataList. Increment the pointer of * recordDataList. Then, check if there are any ties with this character, and keep * incrementing until we find no more ties. */ switch (sortType) { case 'left': { if (choices.length === battleNo - 1) { choices += '0'; } recordData('left'); while (tiedDataList[recordDataList[pointer - 1]] != -1) { recordData('left'); } break; } case 'right': { if (choices.length === battleNo - 1) { choices += '1'; } recordData('right'); while (tiedDataList[recordDataList [pointer - 1]] != -1) { recordData('right'); } break; } /** * For picking 'tie' (i.e. heretics): * * Proceed as if we picked the 'left' character. Then, we record the right character's * index value into the list of ties (at the left character's index) and then proceed * as if we picked the 'right' character. */ case 'tie': { if (choices.length === battleNo - 1) { choices += '2'; } recordData('left'); while (tiedDataList[recordDataList[pointer - 1]] != -1) { recordData('left'); } tiedDataList[recordDataList[pointer - 1]] = sortedIndexList[rightIndex][rightInnerIndex]; recordData('right'); while (tiedDataList[recordDataList [pointer - 1]] != -1) { recordData('right'); } break; } default: return; } /** * Once we reach the limit of the 'right' character list, we * insert all of the 'left' characters into the record, or vice versa. */ const leftListLen = sortedIndexList[leftIndex].length; const rightListLen = sortedIndexList[rightIndex].length; if (leftInnerIndex < leftListLen && rightInnerIndex === rightListLen) { while (leftInnerIndex < leftListLen) { recordData('left'); } } else if (leftInnerIndex === leftListLen && rightInnerIndex < rightListLen) { while (rightInnerIndex < rightListLen) { recordData('right'); } } /** * Once we reach the end of both 'left' and 'right' character lists, we can remove * the arrays from the initial mergesort array, since they are now recorded. This * record is a sorted version of both lists, so we can replace their original * (unsorted) parent with a sorted version. Purge the record afterwards. */ if (leftInnerIndex === leftListLen && rightInnerIndex === rightListLen) { for (let i = 0; i < leftListLen + rightListLen; i++) { sortedIndexList[parentIndexList[leftIndex]][i] = recordDataList[i]; } sortedIndexList.pop(); sortedIndexList.pop(); leftIndex = leftIndex - 2; rightIndex = rightIndex - 2; leftInnerIndex = 0; rightInnerIndex = 0; sortedIndexList.forEach((val, idx) => recordDataList[idx] = 0); pointer = 0; } /** * If, after shifting the 'left' index on the sorted list, we reach past the beginning * of the sorted array, that means the entire array is now sorted. The original unsorted * array in index 0 is now replaced with a sorted version, and we will now output this. */ if (leftIndex < 0) { timeTaken = timeTaken || new Date().getTime() - timestamp; progressBar(`Battle No. ${battleNo} - Completed!`, 100); result(); } else { battleNo++; display(); } } /** * Records data in recordDataList. * * @param {'left'|'right'} sortType Record from the left or the right character array. */ function recordData(sortType) { if (sortType === 'left') { recordDataList[pointer] = sortedIndexList[leftIndex][leftInnerIndex]; leftInnerIndex++; } else { recordDataList[pointer] = sortedIndexList[rightIndex][rightInnerIndex]; rightInnerIndex++; } pointer++; sortedNo++; } /** * Modifies the progress bar. * * @param {string} indicator * @param {number} percentage */ function progressBar(indicator, percentage) { document.querySelector('.progressbattle').innerHTML = indicator; document.querySelector('.progressfill').style.width = `${percentage}%`; document.querySelector('.progresstext').innerHTML = `${percentage}%`; } /** * Shows the result of the sorter. * * @param {number} [imageNum=3] Number of images to display. Defaults to 3. */ function result(imageNum = 3) { document.querySelectorAll('.finished.button').forEach(el => el.style.display = 'block'); document.querySelector('.image.selector').style.display = 'block'; document.querySelector('.time.taken').style.display = 'block'; document.querySelectorAll('.sorting.button').forEach(el => el.style.display = 'none'); document.querySelectorAll('.sort.text').forEach(el => el.style.display = 'none'); document.querySelector('.options').style.display = 'none'; document.querySelector('.info').style.display = 'none'; const header = '