464 lines
12 KiB
JavaScript
464 lines
12 KiB
JavaScript
(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));
|