(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, resistance) => { charStats.armor = { name, defense, resistance, }; 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'), { fire: attrToNum('res-fire'), ice: attrToNum('res-ice'), thunder: attrToNum('res-thunder'), vacuum: attrToNum('res-vacuum'), debuff: attrToNum('res-debuff'), }); 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(); // scale all enemy images to fit in a 64x64 square $('.enemy-sprite').each((i, el) => { const $el = $(el); const width = $el.width(); const height = $el.height(); const maxDimension = 64; const scaleFactor = width < maxDimension && height < maxDimension ? 1 : (width > height ? maxDimension / width : maxDimension / height ); const newWidth = width * scaleFactor; const newHeight = height * scaleFactor; let translateX = ((newWidth - width) / 2); let translateY = ((newHeight - height) / 2); translateY -= (newHeight - maxDimension) / 2; translateX -= (newWidth - maxDimension) / 2; $el.css({ transform: `scaleX(${scaleFactor}) scaleY(${scaleFactor})`, top: translateY + 'px', left: translateX + 'px', }); }); const $enemyInfoModal = $('#enemy-info-modal'); if ($enemyInfoModal.length) { let rowData; $enemyInfoModal.find('.apply-runes-form input[type="checkbox"]').on('change', (e) => { const shouldApplyRunes = e.target.checked; if (rowData) { if (shouldApplyRunes) { rowData.guard /= 2; rowData.power /= 2; rowData.speed -= 50; rowData.hp /= 2; rowData.magic -= 50; } else { rowData.guard *= 2; rowData.power *= 2; rowData.speed += 50; rowData.hp *= 2; rowData.magic += 50; } refreshModal(); } }); const refreshModal = () => { rowData.spells = Array.isArray(rowData.spells) ? rowData.spells : rowData.spells.split(','); const calc = window.saga.calc; 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 && charStats.guard === null && !charStats.armor && !charStats.accessory) { $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 = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory); const withDefend = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerDefending: true, }); const withGuard = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { targetGuardUp: true, }); const withDefendGuard = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { targetGuardUp: true, attackerDefending: true, }); const withPower = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerPowerUp: true, }); const withPowerDefend = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerDefending: true, attackerPowerUp: true, }); const withPowerGuard = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerPowerUp: true, targetGuardUp: true, }); const withPowerGuardDefend = calc.physicalAttack(powerInnate, powerWeapon, guardInnate, guardArmor, guardAccessory, { attackerDefending: true, 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-defend').text(dmgRange(withDefend)); $enemyInfoModal.find('.physical-dmg-defend-guard-up').text(dmgRange(withDefendGuard)); $enemyInfoModal.find('.physical-dmg-power-up').text(dmgRange(withPower)); $enemyInfoModal.find('.physical-dmg-power-up-guard-up').text(dmgRange(withPowerGuard)); $enemyInfoModal.find('.physical-dmg-defend-power-up').text(dmgRange(withPowerDefend)); $enemyInfoModal.find('.physical-dmg-defend-power-up-guard-up').text(dmgRange(withPowerGuardDefend)); // enemy attack const enemyPower = rowData.power; const charGuard = charStats.guard || 0; const charArmor = charStats.armor ? charStats.armor.defense : 0; const charAccessory = charStats.accessory ? charStats.accessory.defense : 0; const enemyDef = calc.physicalAttack(enemyPower, 0, charGuard, charArmor, charAccessory,); const enemyDefDefend = calc.physicalAttack(enemyPower, 0, charGuard, charArmor, charAccessory, { targetDefending: true, }); const enemyDefDefendGuardUp = calc.physicalAttack(enemyPower, 0, charGuard, charArmor, charAccessory, { targetDefending: true, targetGuardUp: true, }); const enemyDefGuardUp = calc.physicalAttack(enemyPower, 0, charGuard, charArmor, charAccessory, { targetGuardUp: true, }); $enemyInfoModal.find('.physical-enemy-dmg').text(dmgRange(enemyDef)); $enemyInfoModal.find('.physical-enemy-dmg-guard-up').text(dmgRange(enemyDefGuardUp)); $enemyInfoModal.find('.physical-enemy-dmg-defend').text(dmgRange(enemyDefDefend)); $enemyInfoModal.find('.physical-enemy-dmg-defend-guard-up').text(dmgRange(enemyDefDefendGuardUp)); } 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 charMagic = charStats.magic; const enemyMagic = rowData.magic; const enemyRes = { 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 = enemyRes[spell.element.toLowerCase()]; const def = calc.magicalAttack(charMagic, enemyMagic, elementalRes, resArmor, resAccessory, spell.power); const magicUp = calc.magicalAttack(charMagic, enemyMagic, 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 = calc.hpCatcherAttack(charStats.magic, 999, 0, rowData.hp); let magicUp = 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 = calc.mpCatcherAttack(charStats.magic, 999, 0, rowData.mp); magicUp = 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)); $enemyInfoModal.find('.enemy-spells tbody tr') .hide() .filter((i, row) => { return rowData.spells.includes(row.getAttribute('data-name')); }) .show(); window.saga.spells.filter(x => !!x.power && rowData.spells.includes(x.name)).forEach((spell) => { const elementalRes = 0; const resArmor = charStats.armor ? charStats.armor.resistance[spell.element.toLowerCase()] : 0; const resAccessory = charStats.accessory ? charStats.accessory.resistance[spell.element.toLowerCase()] : 0; let def = calc.magicalAttack(enemyMagic, charMagic, elementalRes, resArmor, resAccessory, spell.power); let magicUp = calc.magicalAttack(enemyMagic, charMagic, elementalRes, resArmor, resAccessory, spell.power, { targetMagicUp: true, }); $enemyInfoModal.find(`.enemy-spells .magic-enemy-dmg-${spell.name}`).text(magicDmg(def)); $enemyInfoModal.find(`.enemy-spells .magic-enemy-dmg-${spell.name}-magic-up`).text(magicDmg(magicUp)); }); // hpcatcher def = calc.hpCatcherAttack(rowData.magic, 999, 0, rowData.hp); magicUp = calc.hpCatcherAttack(rowData.magic, 999, 0, rowData.hp, { targetMagicUp: true, }); prefix = '.magic-enemy-dmg-HPCatcher'; $enemyInfoModal.find(prefix).text(magicDmg(def)); $enemyInfoModal.find(prefix + '-magic-up').text(magicDmg(magicUp)); // mpcatcher def = calc.mpCatcherAttack(rowData.magic, 999, 0, rowData.mp); magicUp = calc.mpCatcherAttack(rowData.magic, 999, 0, rowData.mp, { attackerMagicUp: true, }); prefix = '.magic-enemy-dmg-MPCatcher'; $enemyInfoModal.find(prefix).text(magicDmg(def)); $enemyInfoModal.find(prefix + '-magic-up').text(magicDmg(magicUp)); } if (charStats.speed !== null) { const hitRate = calc.hitRate(charStats.speed, rowData.speed); const hitRateSpeed = calc.hitRate(charStats.speed, rowData.speed, { attackerSpeedUp: true, }); const runRate = rowData.cls === 1 ? 0 : calc.runRate(charStats.speed, rowData.speed); const runRateSpeed = rowData.cls === 1 ? 0 : 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 enemyHitRate = calc.hitRate(rowData.speed, charStats.speed); const enemyHitRateSpeed = calc.hitRate(rowData.speed, charStats.speed, { attackerSpeedUp: true, }); const enemyHitRateCharSpeed = calc.hitRate(rowData.speed, charStats.speed, { targetSpeedUp: true, }); const enemyHitRateCharSpeedSpeed = calc.hitRate(rowData.speed, charStats.speed, { attackerSpeedUp: true, targetSpeedUp: true, }); $enemyInfoModal.find('.hit-rate-enemy').text(toPer(enemyHitRate)); $enemyInfoModal.find('.hit-rate-enemy-speed-up').text(toPer(enemyHitRateSpeed)); $enemyInfoModal.find('.hit-rate-enemy-char-speed-up').text(toPer(enemyHitRateCharSpeed)); $enemyInfoModal.find('.hit-rate-enemy-char-speed-up-speed-up').text(toPer(enemyHitRateCharSpeedSpeed)); } const debuffRate = calc.effectSpellHitRate(rowData.resDebuff === 100 ? null : rowData.resDebuff, 0, 0); const vacuumRate = 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.on('hide.bs.modal', () => { $enemyInfoModal.find('.apply-runes-form').hide().find('input').prop('checked', false); }); $table.find('tbody.data td').on('click', (e) => { rowData = rowData = { ...$(e.target).closest('tr').data() }; if (rowData.name === 'Gorsia') { $enemyInfoModal.find('.apply-runes-form').show().prop('checked', false); } else { $enemyInfoModal.find('.apply-runes-form').hide().find('input').prop('checked', false); } refreshModal(); $enemyInfoModal.modal({ show: true, }); }); } }(window));