(function(window) { const $apprenticeFilterForm = $('.apprentice-filter-form'); const $locationFilterForm = $('.location-filter-form'); const $table = $('#main-table'); let charStats = { power: null, guard: null, magic: null, speed: null, weapon: null, armor: null, accessory: null, }; const onCharStatChange = (readFromCookie) => { if (!readFromCookie) { window.Cookies.set('charStats', JSON.stringify(charStats), { sameSite: 'strict', }); } else { } }; window.saga = { sortData: () => { const qs = new URLSearchParams(window.location.search); const col = qs.get('col'); let dir = qs.get('dir'); if (!col) { return; } if (!$table.length) { return; } if (dir !== 'asc' && dir !== 'desc') { dir = 'asc'; } const rows = $table.find('tbody.data tr').toArray(); const firstRow = rows[0]; if (!firstRow) { return; } const coefficient = dir === 'desc' ? -1 : 1; rows.sort((a, b) => { let aVal = a.getAttribute('data-' + col); let bVal = b.getAttribute('data-' + col); const isNumber = !isNaN(Number(aVal)); if (isNumber && aVal) { aVal = Number(aVal); bVal = Number(bVal); } if (aVal === bVal) { if (col === 'name') { return 0; } const aName = a.getAttribute('data-name'); const bName = b.getAttribute('data-name'); if (typeof(aName) === 'string' && typeof(bName) === 'string') { return aName.localeCompare(bName) * coefficient; } return 0; } if (typeof(aVal) === 'number' && typeof(bVal) === 'number') { return (aVal < bVal ? -1 : 1) * coefficient; } if (typeof(aVal) === 'string' && typeof(bVal) === 'string') { return aVal.localeCompare(bVal) * coefficient; } return 0; }); let nextChild = null; const headers = $table.find('tr.header th').removeClass('sorted').toArray(); const highlightedIndex = headers.findIndex(cell => cell.getAttribute('data-col') === col); if (highlightedIndex === -1) { throw new Error('could not find header column'); } headers[highlightedIndex].classList.add('sorted'); for (let i = rows.length - 1; i >= 0; i--) { const row = rows[i]; if (row === nextChild) { continue; } $(row).find('td.sorted').removeClass('sorted'); $(row).find('td').eq(highlightedIndex).addClass('sorted'); row.parentNode.insertBefore(row, nextChild); nextChild = row; } }, filterApprentices: () => { if (!$apprenticeFilterForm.length) { return; } const checked = []; $apprenticeFilterForm.find('input[type="checkbox"]').toArray().map((input) => { if (input.checked) { checked.push(input.name); } }); window.Cookies.set('apprenticeFilter', checked.join(','), { sameSite: 'strict', }); $table .find('tbody.data tr') .hide() .filter((i, row) => { if (!checked.length) { return true; } const users = ($(row).attr('data-users') || '').split(','); if (!users.length) { return true; } return checked.some((name) => users.includes(name)); }) .show(); }, filterLocations: () => { if (!$locationFilterForm.length) { return; } const checked = []; $locationFilterForm.find('input[type="checkbox"]').toArray().map((input) => { if (input.checked) { checked.push(input.name); } }); window.Cookies.set('locationFilter', checked.join(','), { sameSite: 'strict', }); $table .find('tbody.data tr') .hide() .filter((i, row) => { if (!checked.length) { return true; } const data = $(row).attr('data-locations').split(','); if (!data.length) { return true; } return data.some((name) => checked.includes(name)); }) .show(); }, updateCharStat: (stat, value) => { charStats[stat] = value; onCharStatChange(); }, updateCharWeapon: (name, power) => { charStats.weapon = { name, power, }; onCharStatChange(); }, updateCharArmor: (name, defense) => { charStats.armor = { name, defense, }; onCharStatChange(); }, updateCharAccessory: (name, defense, resistance) => { charStats.accessory = { name, defense, resistance, }; onCharStatChange(); }, calc: {}, }; const checkLocations = () => { if (!$locationFilterForm.length) { return; } try { window.Cookies.get('locationFilter').split(',').forEach((location) => { $locationFilterForm.find(`input[type="checkbox"][name="${location}"]`).prop('checked', true); }); } catch (e) {} }; const checkApprentices = () => { if (!$apprenticeFilterForm.length) { return; } try { window.Cookies.get('apprenticeFilter').split(',').forEach((apprentice) => { $apprenticeFilterForm.find(`input[type="checkbox"][name="${apprentice}"]`).prop('checked', true); }); } catch (e) {} }; const checkCharStats = () => { try { const stats = JSON.parse(window.Cookies.get('charStats')); if (stats.power !== null) { $('#char-power').val(stats.power); } if (stats.guard !== null) { $('#char-guard').val(stats.guard); } if (stats.magic !== null) { $('#char-magic').val(stats.magic); } if (stats.speed !== null) { $('#char-speed').val(stats.speed); } if (stats.weapon !== null) { $('#char-weapon').val(stats.weapon.name); } if (stats.armor !== null) { $('#char-armor').val(stats.armor.name); } if (stats.accessory !== null) { $('#char-accessory').val(stats.accessory.name); } charStats = stats; } catch (e) {} }; const update = () => { checkLocations(); checkApprentices(); checkCharStats(); window.saga.sortData(); window.saga.filterApprentices(); window.saga.filterLocations(); }; window.addEventListener('popstate', update); $('.sortable-links a').click(function(e) { e.preventDefault(); window.history.replaceState(null, '', $(this).attr('href')); window.saga.sortData(); }); $apprenticeFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterApprentices); $locationFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterLocations); $('#char-stats-modal').find('input,select').on('change', (e) => { const value = e.target.value; const stat = e.target.id.replace(/^char-/, ''); const attrToNum = (name) => Number(e.target.querySelector(`option[value="${value}"]`).getAttribute('data-' + name)); switch (stat) { case 'power': case 'guard': case 'magic': case 'speed': window.saga.updateCharStat(stat, Number(value)); break; case 'weapon': window.saga.updateCharWeapon(value, attrToNum('power')); break; case 'armor': window.saga.updateCharArmor(value, attrToNum('defense')); break; case 'accessory': window.saga.updateCharAccessory( value, attrToNum('defense'), { fire: attrToNum('res-fire'), ice: attrToNum('res-ice'), thunder: attrToNum('res-thunder'), vacuum: attrToNum('res-vacuum'), debuff: attrToNum('res-debuff'), }, ); break; } }); update(); const $enemyInfoModal = $('#enemy-info-modal'); if ($enemyInfoModal.length) { $table.find('tbody.data td').on('click', (e) => { const rowData = $(e.target).closest('tr').data(); Object.keys(rowData).forEach((key) => { const cls = key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); $enemyInfoModal.find('.' + cls).text(rowData[key]); }); if (charStats.power === null && charStats.weapon === null) { $enemyInfoModal.find('[class^="physical-"]').text('n/a'); } else { const powerInnate = charStats.power || 0; const powerWeapon = charStats.weapon ? charStats.weapon.power : 0; const guardInnate = rowData.guard; const guardArmor = 0; const guardAccessory = 0; const def = window.saga.calc.physicalAttack( powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, ); const withGuard = window.saga.calc.physicalAttack( powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { targetGuardUp: true, } ); const withPower = window.saga.calc.physicalAttack( powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerPowerUp: true, } ); const withPowerGuard = window.saga.calc.physicalAttack( powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerPowerUp: true, targetGuardUp: true, } ); const dmgRange = (dmg) => `${Math.floor(dmg.normal.min)}-${Math.ceil(dmg.normal.max)}`; $enemyInfoModal.find('.physical-dmg').text(dmgRange(def)); $enemyInfoModal.find('.physical-dmg-guard-up').text(dmgRange(withGuard)); $enemyInfoModal.find('.physical-dmg-power-up').text(dmgRange(withPower)); $enemyInfoModal.find('.physical-dmg-power-up-guard-up').text(dmgRange(withPowerGuard)); } const magicDmg = (dmg) => `${Math.floor(dmg.min)}-${Math.ceil(dmg.max)}`; const toPer = x => Math.round(x) + '%'; if (charStats.magic === null) { $enemyInfoModal.find('[class^="magic-"]').text('n/a'); } else { const attackerMagic = charStats.magic; const targetMagic = rowData.magic; const res = { ice: rowData.resIce, fire: rowData.resFire, thunder: rowData.resThunder, vacuum: rowData.resVacuum, debuff: rowData.resDebuff, }; const resArmor = 0; const resAccessory = 0; window.saga.spells.filter(x => !!x.power).forEach((spell) => { const elementalRes = res[spell.element.toLowerCase()]; const def = window.saga.calc.magicalAttack(attackerMagic, targetMagic, elementalRes, resArmor, resAccessory, spell.power); const magicUp = window.saga.calc.magicalAttack(attackerMagic, targetMagic, elementalRes, resArmor, resAccessory, spell.power, { attackerMagicUp: true, }); const prefix = '.magic-dmg-' + spell.name; $enemyInfoModal.find(prefix).text(magicDmg(def)); $enemyInfoModal.find(prefix + '-magic-up').text(magicDmg(magicUp)); }); // hpcatcher let def = window.saga.calc.hpCatcherAttack(charStats.magic, 999, 0, rowData.hp); let magicUp = window.saga.calc.hpCatcherAttack(charStats.magic, 999, 0, rowData.hp, { attackerMagicUp: true, }); let prefix = '.magic-dmg-HPCatcher'; $enemyInfoModal.find(prefix).text(magicDmg(def)); $enemyInfoModal.find(prefix + '-magic-up').text(magicDmg(magicUp)); // mpcatcher def = window.saga.calc.mpCatcherAttack(charStats.magic, 999, 0, rowData.mp); magicUp = window.saga.calc.mpCatcherAttack(charStats.magic, 999, 0, rowData.mp, { attackerMagicUp: true, }); prefix = '.magic-dmg-MPCatcher'; $enemyInfoModal.find(prefix).text(magicDmg(def)); $enemyInfoModal.find(prefix + '-magic-up').text(magicDmg(magicUp)); } if (charStats.speed !== null) { const hitRate = window.saga.calc.hitRate(charStats.speed, rowData.speed); const hitRateSpeed = window.saga.calc.hitRate(charStats.speed, rowData.speed, { attackerSpeedUp: true, }); const runRate = window.saga.calc.runRate(charStats.speed, rowData.speed); const runRateSpeed = window.saga.calc.runRate(charStats.speed, rowData.speed, { attackerSpeedUp: true, }); $enemyInfoModal.find('.run-rate').text(toPer(runRate)); $enemyInfoModal.find('.run-rate-speed-up').text(toPer(runRateSpeed)); $enemyInfoModal.find('.hit-rate').text(toPer(hitRate)); $enemyInfoModal.find('.hit-rate-speed-up').text(toPer(hitRateSpeed)); } const debuffRate = window.saga.calc.effectSpellHitRate(rowData.resDebuff === 100 ? null : rowData.resDebuff, 0, 0); const vacuumRate = window.saga.calc.effectSpellHitRate(rowData.resVacuum === 100 ? null : rowData.resVacuum, 0, 0); $enemyInfoModal.find('.debuff-rate').text(toPer(debuffRate)); $enemyInfoModal.find('.vacuum-rate').text(toPer(vacuumRate)); $enemyInfoModal.modal({ show: true, }); }); } else { } }(window));