7th-saga/web/static/saga.js

608 lines
18 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, 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));