diff --git a/data/calc.js b/data/calc.js deleted file mode 100644 index bf3e315..0000000 --- a/data/calc.js +++ /dev/null @@ -1,181 +0,0 @@ -// https://gamefaqs.gamespot.com/snes/563501-the-7th-saga/faqs/54038 - -exports.physicalAttack = ( - attackerPowerInnate, - attackerPowerWeapon, - targetGuardInnate, - targetGuardArmor, - targetGuardAccessory, - options = {}, -) => { - let attackPower = attackerPowerInnate; - if (options.attackerDefending) { - attackPower *= 1.5; - } - if (options.attackerPowerUp) { - attackPower *= 2; - } - - let weaponPower = attackerPowerWeapon; - if (options.attackerDefending) { - weaponPower *= 1.5; - } - - let guardPower = targetGuardInnate; - if (options.targetGuardDown) { - guardPower /= 2; - } - - const totalAttackPower = attackPower + weaponPower; - const totalGuardPower = guardPower + targetGuardArmor + targetGuardAccessory; - - let averageDamage = totalAttackPower - (totalGuardPower / 2); - - if (options.targetGuardUp) { - averageDamage /= 2; - } - if (options.targetDefending) { - averageDamage /= 2; - } - - let minDamage = averageDamage * 0.75; - let maxDamage = averageDamage * 1.25; - - const absoluteMin = 1; - - return { - normal: { - avg: Math.max(absoluteMin, averageDamage), - min: Math.max(absoluteMin, minDamage), - max: Math.max(absoluteMin, maxDamage), - }, - critical: { - avg: Math.max(absoluteMin, averageDamage * 2), - min: Math.max(absoluteMin, minDamage * 2), - max: Math.max(absoluteMin, maxDamage * 2), - }, - }; -}; - -exports.magicalAttack = ( - attackerMagicInnate, - targetMagicInnate, - targetResistanceInnate, - targetResistanceArmor, - targetResistanceAccessory, - spellPower, - options = {}, -) => { - let attackerPower = attackerMagicInnate; - - let targetPower = targetMagicInnate; - if (options.targetMagicUp) { - targetPower += 40; - } - - const targetResistance = 100 - targetResistanceInnate - targetResistanceArmor - targetResistanceAccessory; - - const spellBonusAttackPower = attackerPower + (options.attackerMagicUp ? 40 : 0); - const spellBonus = ((spellBonusAttackPower / 2) + spellPower) * (targetResistance / 100); - - const averageDamage = attackerPower - targetPower + spellBonus; - - return { - avg: Math.max(1, averageDamage), - min: Math.max(1, averageDamage * 0.75), - max: Math.max(1, averageDamage * 1.25), - }; -}; - -exports.hpCatcherAttack = ( - attackerMagicInnate, - attackerMaxHP, - attackerCurrentHP, - targetCurrentHP, - options = {}, -) => { - let attackerPower = attackerMagicInnate; - if (options.attackerMagicUp) { - attackerPower += 40; - } - - const adjust = dmg => Math.min(attackerMaxHP - attackerCurrentHP, Math.min(Math.min(dmg, 50), targetCurrentHP)); - const minDamage = attackerPower / 2; - - return { - min: adjust(minDamage), - max: adjust(minDamage + 15), - }; -}; - -exports.mpCatcherAttack = ( - attackerMagicInnate, - attackerMaxMP, - attackerCurrentMP, - targetCurrentMP, - options = {}, -) => { - let attackerPower = attackerMagicInnate; - if (options.attackerMagicUp) { - attackerPower += 40; - } - - const adjust = dmg => Math.min(attackerMaxMP - attackerCurrentMP, Math.min(Math.min(dmg, 40), targetCurrentMP)); - const minDamage = attackerPower / 2; - - return { - min: adjust(minDamage), - max: adjust(minDamage + 15), - }; -}; - -exports.effectSpellHitRate = ( - targetResistanceInnate, - targetResistanceArmor, - targetResistanceAccessory, -) => { - if (targetResistanceInnate === null) { - // e.g. "Class 1" enemies - return 0; - } - - return Math.max(5, 100 - targetResistanceInnate - targetResistanceArmor - targetResistanceAccessory); -}; - -exports.hitRate = ( - attackerSpeedInnate, - targetSpeedInnate, - options = {}, -) => { - let attackerSpeed = attackerSpeedInnate; - if (options.attackerSpeedUp) { - attackerSpeed += 30; - } - - let targetSpeed = targetSpeedInnate; - if (options.targetSpeedUp) { - targetSpeed += 30; - } - - const hitRate = 85 + (0.8 * (attackerSpeed - targetSpeed)); - return Math.max(10, Math.min(98, hitRate)); -}; - -exports.runRate = ( - attackerSpeedInnate, - targetSpeedInnate, - options = {}, -) => { - let attackerSpeed = attackerSpeedInnate; - if (options.attackerSpeedUp) { - attackerSpeed += 30; - } - - let targetSpeed = targetSpeedInnate; - if (options.targetSpeedUp) { - targetSpeed += 30; - } - - const runRate = 0.25 + (1.6 * (attackerSpeed - targetSpeed)); - return Math.max(0.1, Math.min(0.8, runRate)); -}; diff --git a/data/parse-enemies.js b/data/parse-enemies.js index 8394648..0ef1e17 100644 --- a/data/parse-enemies.js +++ b/data/parse-enemies.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); -const {spells} = require('./spells'); +const {spells} = require('../web/static/spells'); const spellMap = spells.reduce((map, spell) => { map[spell.name] = spell; diff --git a/data/spells.js b/data/spells.js deleted file mode 100644 index 02f1202..0000000 --- a/data/spells.js +++ /dev/null @@ -1,82 +0,0 @@ -const attackSpell = (name, mp, power, element, multiple) => { - return { - type: 'Attack', - name, - mp, - power, - element, - targets: multiple ? 'multi' : 'single', - }; -}; - -const effectSpell = (name, mp, element, effect, multiple) => { - return { - type: 'Effect', - name, - mp, - element, - effect, - targets: multiple ? 'multi' : 'single', - }; -}; - -const healSpell = (name, mp, healingPower, effect) => { - return { - type: 'Healing', - name, - mp, - healingPower, - effect, - targets: 'single', - }; -}; - -const supportSpell = (name, mp, effect, locations) => { - return { - type: 'Support', - name, - mp, - effect, - locations, - targets: 'single', - }; -}; - -exports.spells = [ - attackSpell('Fire1', 3, 30, 'Fire'), - attackSpell('Fire2', 12, 70, 'Fire'), - attackSpell('Ice1', 3, 36, 'Ice'), - attackSpell('Ice2', 12, 80, 'Ice'), - attackSpell('Laser1', 3, 30, 'Thunder'), - attackSpell('Laser2', 10, 50, 'Thunder'), - attackSpell('Laser3', 20, 100, 'Thunder'), - attackSpell('Firebird', 14, 30, 'Fire', true), - attackSpell('Fireball', 32, 50, 'Fire', true), - attackSpell('Blizzard1', 14, 34, 'Ice', true), - attackSpell('Blizzard2', 32, 60, 'Ice', true), - attackSpell('Thunder1', 10, 40, 'Thunder', true), - attackSpell('Thunder2', 40, 75, 'Thunder', true), - - effectSpell('Petrify', 10, 'Debuff', 'Petrification'), - effectSpell('Poison', 0, 'Debuff', 'Poison (monsters only)'), - effectSpell('Defense2', 5, 'Debuff', 'Halves guard'), - effectSpell('HPCatcher', 6, 'Debuff', 'Drains up to 50 HP and heals caster'), - effectSpell('MPCatcher', 8, 'Debuff', 'Drains up to 40 MP and heals caster'), - effectSpell('Vacuum1', 30, 'Vacuum', 'Instant death'), - effectSpell('Vacuum2', 60, 'Vacuum', 'Instant death', true), - - healSpell('Heal1', 4, 40, 'Restores 40 HP'), - healSpell('Heal2', 18, 90, 'Restores 90 HP'), - healSpell('Heal3', 34, 'max', 'Restores all HP'), - healSpell('Elixir', 120, 'max', 'Restores all HP and MP'), - healSpell('Revive1', 40, 'max', 'Restores all HP to dead ally, fails ~50% of the time'), - healSpell('Revive2', 90, 'max', 'Restores all HP to dead ally, always succeeds'), - - supportSpell('Purify', 8, 'Removes petrification and poison', [ 'Battle', 'Map' ]), - supportSpell('Defense1', 5, 'Halves physical damage from enemy', [ 'Battle' ]), - supportSpell('Power', 6, 'Doubles power', [ 'Battle' ]), - supportSpell('Agility', 3, 'Increases Speed by 30', [ 'Battle' ]), - supportSpell('F.Shield', 16, 'Nullifies next attack spell', [ 'Battle' ]), - supportSpell('Protect', 20, 'Nullifies next Vacuum spell', [ 'Battle' ]), - supportSpell('Exit', 30, 'Escape cave/dungeon immediately', [ 'Map' ]), -]; diff --git a/web/server.js b/web/server.js index cdb4e22..6f8afd9 100644 --- a/web/server.js +++ b/web/server.js @@ -2,8 +2,7 @@ const express = require('express'); const path = require('path'); const enemies = require('../data/enemies.json'); -const calc = require('../data/calc'); -const spells = require('../data/spells'); +const spells = require('./static/spells'); const items = require('../data/items'); const exp = require('../data/exp'); @@ -15,29 +14,67 @@ app.set('cache views', false); app.use(express.static(path.join(__dirname))); +const render = (res, view, params) => { + res.render(view, { + ...params, + charSpells: spells.spells.concat([]).sort((a, b) => { + if (a.power && b.power) { + return -1; + } + if (!a.power && b.power) { + return 1; + } + + if (a.power === b.power) { + return a.name.localeCompare(b.name); + } + + return a.power < b.power ? -1 : 1; + }), + charWeapons: items.weapons.concat([]).sort((a, b) => { + if (a.attack === b.attack) { + return a.name.localeCompare(b.name); + } + return a.attack === b.attack ? 0 : (a.attack < b.attack ? -1 : 1); + }), + charArmor: items.armor.concat([]).sort((a, b) => { + if (a.defense === b.defense) { + return a.name.localeCompare(b.name); + } + return a.defense === b.defense ? 0 : (a.defense < b.defense ? -1 : 1); + }), + charAccessories: items.accessories.concat([]).sort((a, b) => { + if (a.defense === b.defense) { + return a.name.localeCompare(b.name); + } + return a.defense === b.defense ? 0 : (a.defense < b.defense ? -1 : 1); + }), + }); +} + app.get([ '/', '/enemies' ], (req, res) => { - res.render('enemies', { + render(res, 'enemies', { context: 'enemies', enemies, }); }); app.get('/spells', (req, res) => { - res.render('spells', { + render(res, 'spells', { context: 'spells', spells: spells.spells.sort((a, b) => a.name.localeCompare(b.name)), }); }); app.get('/exp', (req, res) => { - res.render('exp', { + render(res, 'exp', { context: 'exp', exp: exp.exp, }); }); app.get('/weapons', (req, res) => { - res.render('weapons', { + render(res, 'weapons', { context: 'weapons', weapons: items.weapons.sort((a, b) => { if (a.attack === b.attack) { @@ -49,7 +86,7 @@ app.get('/weapons', (req, res) => { }); app.get('/armor', (req, res) => { - res.render('armor', { + render(res, 'armor', { context: 'armor', armor: items.armor.sort((a, b) => { if (a.defense === b.defense) { @@ -61,14 +98,14 @@ app.get('/armor', (req, res) => { }); app.get('/accessories', (req, res) => { - res.render('accessories', { + render(res, 'accessories', { context: 'accessories', accessories: items.accessories, }); }); app.get('/items', (req, res) => { - res.render('items', { + render(res, 'items', { context: 'items', items: items.items, }); diff --git a/web/static/7th-saga.css b/web/static/7th-saga.css index cd9f41e..ed50f93 100644 --- a/web/static/7th-saga.css +++ b/web/static/7th-saga.css @@ -49,7 +49,7 @@ table.sticky-header { table.sticky-header tr.header th { position: sticky; - top: 37px; + top: 44px; background-color: #fcfac8; z-index: 2; box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); @@ -58,3 +58,7 @@ table.sticky-header tr.header th { th.sorted, td.sorted { background-color: #dae5f6 !important; } + +table.row-clickable tbody.data td { + cursor: pointer; +} diff --git a/web/static/calc.js b/web/static/calc.js new file mode 100644 index 0000000..a75ba56 --- /dev/null +++ b/web/static/calc.js @@ -0,0 +1,184 @@ +// https://gamefaqs.gamespot.com/snes/563501-the-7th-saga/faqs/54038 + +(function(exports) { + exports.physicalAttack = ( + attackerPowerInnate, + attackerPowerWeapon, + targetGuardInnate, + targetGuardArmor, + targetGuardAccessory, + options = {}, + ) => { + let attackPower = attackerPowerInnate; + if (options.attackerDefending) { + attackPower *= 1.5; + } + if (options.attackerPowerUp) { + attackPower *= 2; + } + + let weaponPower = attackerPowerWeapon; + if (options.attackerDefending) { + weaponPower *= 1.5; + } + + let guardPower = targetGuardInnate; + if (options.targetGuardDown) { + guardPower /= 2; + } + + const totalAttackPower = attackPower + weaponPower; + const totalGuardPower = guardPower + targetGuardArmor + targetGuardAccessory; + + let averageDamage = totalAttackPower - (totalGuardPower / 2); + + if (options.targetGuardUp) { + averageDamage /= 2; + } + if (options.targetDefending) { + averageDamage /= 2; + } + + let minDamage = averageDamage * 0.75; + let maxDamage = averageDamage * 1.25; + + const absoluteMin = 1; + + return { + normal: { + avg: Math.max(absoluteMin, averageDamage), + min: Math.max(absoluteMin, minDamage), + max: Math.max(absoluteMin, maxDamage), + }, + critical: { + avg: Math.max(absoluteMin, averageDamage * 2), + min: Math.max(absoluteMin, minDamage * 2), + max: Math.max(absoluteMin, maxDamage * 2), + }, + }; + }; + + exports.magicalAttack = ( + attackerMagicInnate, + targetMagicInnate, + targetResistanceInnate, + targetResistanceArmor, + targetResistanceAccessory, + spellPower, + options = {}, + ) => { + let attackerPower = attackerMagicInnate; + + let targetPower = targetMagicInnate; + if (options.targetMagicUp) { + targetPower += 40; + } + + const targetResistance = 100 - targetResistanceInnate - targetResistanceArmor - targetResistanceAccessory; + + const spellBonusAttackPower = attackerPower + (options.attackerMagicUp ? 40 : 0); + const spellBonus = ((spellBonusAttackPower / 2) + spellPower) * (targetResistance / 100); + + const averageDamage = attackerPower - targetPower + spellBonus; + + return { + avg: Math.max(1, averageDamage), + min: Math.max(1, averageDamage * 0.75), + max: Math.max(1, averageDamage * 1.25), + }; + }; + + exports.hpCatcherAttack = ( + attackerMagicInnate, + attackerMaxHP, + attackerCurrentHP, + targetCurrentHP, + options = {}, + ) => { + let attackerPower = attackerMagicInnate; + if (options.attackerMagicUp) { + attackerPower += 40; + } + + const adjust = dmg => Math.min(attackerMaxHP - attackerCurrentHP, Math.min(Math.min(dmg, 50), targetCurrentHP)); + const minDamage = attackerPower / 2; + + return { + min: adjust(minDamage), + max: adjust(minDamage + 15), + }; + }; + + exports.mpCatcherAttack = ( + attackerMagicInnate, + attackerMaxMP, + attackerCurrentMP, + targetCurrentMP, + options = {}, + ) => { + let attackerPower = attackerMagicInnate; + if (options.attackerMagicUp) { + attackerPower += 40; + } + + const adjust = dmg => Math.min(attackerMaxMP - attackerCurrentMP, Math.min(Math.min(dmg, 40), targetCurrentMP)); + const minDamage = attackerPower / 2; + + return { + min: adjust(minDamage), + max: adjust(minDamage + 15), + }; + }; + + exports.effectSpellHitRate = ( + targetResistanceInnate, + targetResistanceArmor, + targetResistanceAccessory, + ) => { + if (targetResistanceInnate === null) { + // e.g. "Class 1" enemies + return 0; + } + + return Math.max(5, 100 - targetResistanceInnate - targetResistanceArmor - targetResistanceAccessory); + }; + + exports.hitRate = ( + attackerSpeedInnate, + targetSpeedInnate, + options = {}, + ) => { + let attackerSpeed = attackerSpeedInnate; + if (options.attackerSpeedUp) { + attackerSpeed += 30; + } + + let targetSpeed = targetSpeedInnate; + if (options.targetSpeedUp) { + targetSpeed += 30; + } + + const hitRate = 85 + (0.8 * (attackerSpeed - targetSpeed)); + return Math.max(10, Math.min(98, hitRate)); + }; + + exports.runRate = ( + attackerSpeedInnate, + targetSpeedInnate, + options = {}, + ) => { + let attackerSpeed = attackerSpeedInnate; + if (options.attackerSpeedUp) { + attackerSpeed += 30; + } + + let targetSpeed = targetSpeedInnate; + if (options.targetSpeedUp) { + targetSpeed += 30; + } + + const runRate = 0.25 + (1.6 * (attackerSpeed - targetSpeed)); + return Math.max(10, Math.min(80, runRate)); + }; + +}(typeof(module) !== 'undefined' ? module.exports : window.saga.calc)); diff --git a/web/static/images/7th-saga-apprentices.png b/web/static/images/7th-saga-apprentices.png new file mode 100644 index 0000000..354f7ae Binary files /dev/null and b/web/static/images/7th-saga-apprentices.png differ diff --git a/web/static/images/7th-saga-enemies.png b/web/static/images/7th-saga-enemies.png new file mode 100644 index 0000000..92dc9be Binary files /dev/null and b/web/static/images/7th-saga-enemies.png differ diff --git a/web/static/saga.js b/web/static/saga.js index 68362b5..078c340 100644 --- a/web/static/saga.js +++ b/web/static/saga.js @@ -3,9 +3,28 @@ 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: () => { - console.log('sorting data'); const qs = new URLSearchParams(window.location.search); const col = qs.get('col'); let dir = qs.get('dir'); @@ -158,6 +177,33 @@ }) .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 = () => { @@ -184,20 +230,44 @@ } catch (e) {} }; - checkLocations(); - checkApprentices(); + 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) {} + }; - window.saga.sortData(); - window.saga.filterApprentices(); - window.saga.filterLocations(); - - window.addEventListener('popstate', () => { + 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(); @@ -207,4 +277,187 @@ $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)); diff --git a/web/static/spells.js b/web/static/spells.js new file mode 100644 index 0000000..7106fcf --- /dev/null +++ b/web/static/spells.js @@ -0,0 +1,84 @@ +(function(exports) { + const attackSpell = (name, mp, power, element, multiple) => { + return { + type: 'Attack', + name, + mp, + power, + element, + targets: multiple ? 'multi' : 'single', + }; + }; + + const effectSpell = (name, mp, element, effect, multiple) => { + return { + type: 'Effect', + name, + mp, + element, + effect, + targets: multiple ? 'multi' : 'single', + }; + }; + + const healSpell = (name, mp, healingPower, effect) => { + return { + type: 'Healing', + name, + mp, + healingPower, + effect, + targets: 'single', + }; + }; + + const supportSpell = (name, mp, effect, locations) => { + return { + type: 'Support', + name, + mp, + effect, + locations, + targets: 'single', + }; + }; + + exports.spells = [ + attackSpell('Fire1', 3, 30, 'Fire'), + attackSpell('Fire2', 12, 70, 'Fire'), + attackSpell('Ice1', 3, 36, 'Ice'), + attackSpell('Ice2', 12, 80, 'Ice'), + attackSpell('Laser1', 3, 30, 'Thunder'), + attackSpell('Laser2', 10, 50, 'Thunder'), + attackSpell('Laser3', 20, 100, 'Thunder'), + attackSpell('Firebird', 14, 30, 'Fire', true), + attackSpell('Fireball', 32, 50, 'Fire', true), + attackSpell('Blizzard1', 14, 34, 'Ice', true), + attackSpell('Blizzard2', 32, 60, 'Ice', true), + attackSpell('Thunder1', 10, 40, 'Thunder', true), + attackSpell('Thunder2', 40, 75, 'Thunder', true), + + effectSpell('Petrify', 10, 'Debuff', 'Petrification'), + effectSpell('Poison', 0, 'Debuff', 'Poison (monsters only)'), + effectSpell('Defense2', 5, 'Debuff', 'Halves guard'), + effectSpell('HPCatcher', 6, 'Debuff', 'Drains up to 50 HP and heals caster'), + effectSpell('MPCatcher', 8, 'Debuff', 'Drains up to 40 MP and heals caster'), + effectSpell('Vacuum1', 30, 'Vacuum', 'Instant death'), + effectSpell('Vacuum2', 60, 'Vacuum', 'Instant death', true), + + healSpell('Heal1', 4, 40, 'Restores 40 HP'), + healSpell('Heal2', 18, 90, 'Restores 90 HP'), + healSpell('Heal3', 34, 'max', 'Restores all HP'), + healSpell('Elixir', 120, 'max', 'Restores all HP and MP'), + healSpell('Revive1', 40, 'max', 'Restores all HP to dead ally, fails ~50% of the time'), + healSpell('Revive2', 90, 'max', 'Restores all HP to dead ally, always succeeds'), + + supportSpell('Purify', 8, 'Removes petrification and poison', ['Battle', 'Map']), + supportSpell('Defense1', 5, 'Halves physical damage from enemy', ['Battle']), + supportSpell('Power', 6, 'Doubles power', ['Battle']), + supportSpell('Agility', 3, 'Increases Speed by 30', ['Battle']), + supportSpell('F.Shield', 16, 'Nullifies next attack spell', ['Battle']), + supportSpell('Protect', 20, 'Nullifies next Vacuum spell', ['Battle']), + supportSpell('Exit', 30, 'Escape cave/dungeon immediately', ['Map']), + ]; +}(typeof(module) !== 'undefined' ? module.exports : window.saga)); diff --git a/web/views/enemies.pug b/web/views/enemies.pug index 0416c29..0f4195f 100644 --- a/web/views/enemies.pug +++ b/web/views/enemies.pug @@ -1,30 +1,31 @@ extends master.pug block tab-content - table#main-table.table.table-sm.table-borderless.table-striped.table-hover.sticky-header + table#main-table.table.table-sm.table-borderless.table-striped.table-hover.sticky-header.row-clickable tr.header-above th(colspan="2") - th(colspan="3").text-center.bg-info.text-light Reward - th(colspan="7").text-center.bg-secondary.text-light Stats + th(colspan="2").text-center.bg-info.text-light Reward + th(colspan="6").text-center.bg-secondary.text-light Stats th(colspan="5").text-center.bg-dark.text-light Resistance + th(colspan="2") tr.header th.align-middle Img +sortHeader('Name', 'name') +sortHeader('Gold', 'gold') +sortHeader('Exp', 'exp') - th.align-middle Drops +sortHeader('HP', 'hp') +sortHeader('MP', 'mp') +sortHeader('Power', 'power') +sortHeader('Guard', 'guard') +sortHeader('Magic', 'magic') +sortHeader('Speed', 'speed') - th.align-middle Spells +sortHeader('Fire', 'res-fire') +sortHeader('Ice', 'res-ice') +sortHeader('Thunder', 'res-thunder') +sortHeader('Vacuum', 'res-vacuum') +sortHeader('Debuff', 'res-debuff') + th.align-middle Spells + th.align-middle Drops tbody.data: each enemy in enemies tr( data-name=enemy.name @@ -39,30 +40,128 @@ block tab-content data-res-fire=enemy.resistance.fire data-res-ice=enemy.resistance.ice data-res-thunder=enemy.resistance.thunder - data-res-vacuum=enemy.resistance.vacuum - data-res-debuff=enemy.resistance.debuff + data-res-vacuum=(enemy.resistance.vacuum === null ? 100 : enemy.resistance.vacuum) + data-res-debuff=(enemy.resistance.debuff === null ? 100 : enemy.resistance.debuff) ) td td: strong= enemy.name - td: code= enemy.gold - td: code= enemy.exp + td.text-right: code= enemy.gold + td.text-right: code= enemy.exp + td.text-right: code= enemy.hp + td.text-right: code= enemy.mp + td.text-right: code= enemy.power + td.text-right: code= enemy.guard + td.text-right: code= enemy.magic + td.text-right: code= enemy.speed + td.text-right: code= enemy.resistance.fire + td.text-right: code= enemy.resistance.ice + td.text-right: code= enemy.resistance.thunder + td.text-right: code= enemy.resistance.vacuum === null ? 100 : enemy.resistance.vacuum + td.text-right: code= enemy.resistance.debuff === null ? 100 : enemy.resistance.debuff + td + +na(enemy.spells.length) + ul.list-horizontal: each spell in enemy.spells + li= spell td +na(Object.keys(enemy.drops).length) ul.list-horizontal: each rate, item in enemy.drops li = item + ' (' + (rate * 100).toFixed(2) + '%)' - td: code= enemy.hp - td: code= enemy.mp - td: code= enemy.power - td: code= enemy.guard - td: code= enemy.magic - td: code= enemy.speed - td - +na(enemy.spells.length) - ul.list-horizontal: each spell in enemy.spells - li= spell - td: code= enemy.resistance.fire - td: code= enemy.resistance.ice - td: code= enemy.resistance.thunder - td: code= enemy.resistance.vacuum - td: code= enemy.resistance.debuff + + div#enemy-info-modal.modal(tabindex="-1") + div.modal-dialog.modal-lg + div.modal-content + div.modal-header + h5.modal-title + span.name + div + | #[span.gold]/#[span.exp] + div.modal-body + div.row + div.col-6 + table.table.table-horizontal + tr + th(colspan="2") Stats + th(colspan="2") Resistances + tr + th HP/MP + td + span.hp + | / + span.mp + th Fire + td.res-fire + tr + th Power + td.power + th Ice + td.res-ice + tr + th Guard + td.guard + th Thunder + td.res-thunder + tr + th Magic + td.magic + th Vacuum + td.res-vacuum + tr + th Speed + td.speed + th Debuff + td.res-debuff + + div.card.bg-light + div.card-body + table.table.table-sm.table-horizontal + tr + th Run rate + td: code.run-rate + th w/ speed up + td: code.run-rate-speed-up + tr + th Hit rate + td: code.hit-rate + th w/ speed up + td: code.hit-rate-speed-up + tr + th Debuff rate + td: code.debuff-rate + th + td + tr + th Vacuum rate + td: code.vacuum-rate + th + td + + div.col-6 + div.card.bg-light + div.card-body + table.table.table-horizontal.table-sm + tr + th + th + th Default + th Guard up + tr + th(rowspan="2").align-middle Physical + th Normal + td: code.physical-dmg + td: code.physical-dmg-guard-up + tr + th Power up + td: code.physical-dmg-power-up + td: code.physical-dmg-power-up-guard-up + tr + th + th + th Default + th Magic up + each spell in charSpells.filter(x => !!x.power || x.name === 'HPCatcher' || x.name === 'MPCatcher') + tr + th= spell.name + th + td: code(class=("magic-dmg-" + spell.name)) + td: code(class=("magic-dmg-" + spell.name + '-magic-up')) diff --git a/web/views/master.pug b/web/views/master.pug index 3730ed2..ea7ab69 100644 --- a/web/views/master.pug +++ b/web/views/master.pug @@ -37,21 +37,79 @@ html label.form-check-label(for=id)= location div.container-fluid - ul.nav.nav-tabs.mt-4.px-4.position-sticky(style="top: 0; background-color: white; z-index: 1") - li.nav-item: a.nav-link(href="/enemies" class=(context === 'enemies' ? 'active' : '')) Enemies - li.nav-item: a.nav-link(href="/spells" class=(context === 'spells' ? 'active' : '')) Spells - li.nav-item: a.nav-link(href="/items" class=(context === 'items' ? 'active' : '')) Items - li.nav-item: a.nav-link(href="/weapons" class=(context === 'weapons' ? 'active' : '')) Weapons - li.nav-item: a.nav-link(href="/armor" class=(context === 'armor' ? 'active' : '')) Armor - li.nav-item: a.nav-link(href="/accessories" class=(context === 'accessories' ? 'active' : '')) Accessories - li.nav-item: a.nav-link(href="/exp" class=(context === 'exp' ? 'active' : '')) Experience - li.nav-item: a.nav-link(href="/calc" class=(context === 'calc' ? 'active' : '')) Calculations + div.bg-light.position-sticky.pt-2.d-flex.justify-content-between(style="top: 0; background-color: white; z-index: 1") + ul.nav.mr-auto.nav-tabs + li.nav-item: a.nav-link(href="/enemies" class=(context === 'enemies' ? 'active' : '')) Enemies + li.nav-item: a.nav-link(href="/spells" class=(context === 'spells' ? 'active' : '')) Spells + li.nav-item: a.nav-link(href="/items" class=(context === 'items' ? 'active' : '')) Items + li.nav-item: a.nav-link(href="/weapons" class=(context === 'weapons' ? 'active' : '')) Weapons + li.nav-item: a.nav-link(href="/armor" class=(context === 'armor' ? 'active' : '')) Armor + li.nav-item: a.nav-link(href="/accessories" class=(context === 'accessories' ? 'active' : '')) Accessories + li.nav-item: a.nav-link(href="/exp" class=(context === 'exp' ? 'active' : '')) Experience + div + button.btn.btn-secondary.btn-sm(data-toggle="modal" data-target="#char-stats-modal") Character stats… div.tab-content div.tab-pane.show.active.mt-2 block tab-content + + div#char-stats-modal.modal(tabindex="-1") + div.modal-dialog + div.modal-content + div.modal-header + h5.modal-title Character stats + button.close(type="button" data-dismiss="modal") + div.modal-body + div.row + div.col-6 + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-power") Power + div.col-8: input#char-power.form-control(type="number" min="0" step="1" max="999" autocomplete="off") + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-guard") Guard + div.col-8: input#char-guard.form-control(type="number" min="0" step="1" max="999" autocomplete="off") + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-magic") Magic + div.col-8: input#char-magic.form-control(type="number" min="0" step="1" max="255" autocomplete="off") + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-speed") Speed + div.col-8: input#char-speed.form-control(type="number" min="0" step="1" max="255" autocomplete="off") + div.col-6 + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-weapon") Weapon + div.col-8: select#char-weapon.form-control(autocomplete="off") + option(value="") Choose weapon + each weapon in charWeapons + option(value=weapon.name data-power=weapon.attack) + = weapon.name + ' (' + weapon.attack + ')' + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-armor") Armor + div.col-8: select#char-armor.form-control(autocomplete="off") + option(value="") Choose armor + each armor in charArmor + option(value=armor.name data-defense=armor.defense) + = armor.name + ' (' + armor.defense + ')' + div.form-row.form-group + label.col-form-label-sm.col-4(for="char-armor") Accessory + div.col-8: select#char-accessory.form-control(autocomplete="off") + option(value="") Choose accessory + each accessory in charAccessories + option( + value=accessory.name + data-defense=accessory.defense + data-res-fire=accessory.resistance.fire + data-res-ice=accessory.resistance.ice + data-res-thunder=accessory.resistance.thunder + data-res-vacuum=accessory.resistance.vacuum + data-res-debuff=accessory.resistance.debuff + )= accessory.name + ' (' + accessory.defense + ')' + div.modal-footer + button.btn.btn-primary(data-dismiss="modal") Close + script(src="/static/jquery.js") script(src="/static/popper.js") script(src="/static/bootstrap.js") script(src="/static/js.cookie.js") script(src="/static/saga.js") + script(src="/static/calc.js") + script(src="/static/spells.js") diff --git a/web/views/weapons.pug b/web/views/weapons.pug index 74ad77c..9850fdc 100644 --- a/web/views/weapons.pug +++ b/web/views/weapons.pug @@ -2,7 +2,7 @@ extends master.pug block tab-content - div.row.d-flex.justify-content-center: div.col-12.col-md-8 + div.row: div.col-12.col-md-8 +apprenticeFilterForm() table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header thead: tr.header