calculation stuff in ui

This commit is contained in:
tmont 2021-02-21 20:37:37 -08:00
parent 6b40ec0a11
commit 5fe8b2f773
13 changed files with 773 additions and 317 deletions

View File

@ -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));
};

View File

@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const {spells} = require('./spells'); const {spells} = require('../web/static/spells');
const spellMap = spells.reduce((map, spell) => { const spellMap = spells.reduce((map, spell) => {
map[spell.name] = spell; map[spell.name] = spell;

View File

@ -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' ]),
];

View File

@ -2,8 +2,7 @@ const express = require('express');
const path = require('path'); const path = require('path');
const enemies = require('../data/enemies.json'); const enemies = require('../data/enemies.json');
const calc = require('../data/calc'); const spells = require('./static/spells');
const spells = require('../data/spells');
const items = require('../data/items'); const items = require('../data/items');
const exp = require('../data/exp'); const exp = require('../data/exp');
@ -15,29 +14,67 @@ app.set('cache views', false);
app.use(express.static(path.join(__dirname))); 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) => { app.get([ '/', '/enemies' ], (req, res) => {
res.render('enemies', { render(res, 'enemies', {
context: 'enemies', context: 'enemies',
enemies, enemies,
}); });
}); });
app.get('/spells', (req, res) => { app.get('/spells', (req, res) => {
res.render('spells', { render(res, 'spells', {
context: 'spells', context: 'spells',
spells: spells.spells.sort((a, b) => a.name.localeCompare(b.name)), spells: spells.spells.sort((a, b) => a.name.localeCompare(b.name)),
}); });
}); });
app.get('/exp', (req, res) => { app.get('/exp', (req, res) => {
res.render('exp', { render(res, 'exp', {
context: 'exp', context: 'exp',
exp: exp.exp, exp: exp.exp,
}); });
}); });
app.get('/weapons', (req, res) => { app.get('/weapons', (req, res) => {
res.render('weapons', { render(res, 'weapons', {
context: 'weapons', context: 'weapons',
weapons: items.weapons.sort((a, b) => { weapons: items.weapons.sort((a, b) => {
if (a.attack === b.attack) { if (a.attack === b.attack) {
@ -49,7 +86,7 @@ app.get('/weapons', (req, res) => {
}); });
app.get('/armor', (req, res) => { app.get('/armor', (req, res) => {
res.render('armor', { render(res, 'armor', {
context: 'armor', context: 'armor',
armor: items.armor.sort((a, b) => { armor: items.armor.sort((a, b) => {
if (a.defense === b.defense) { if (a.defense === b.defense) {
@ -61,14 +98,14 @@ app.get('/armor', (req, res) => {
}); });
app.get('/accessories', (req, res) => { app.get('/accessories', (req, res) => {
res.render('accessories', { render(res, 'accessories', {
context: 'accessories', context: 'accessories',
accessories: items.accessories, accessories: items.accessories,
}); });
}); });
app.get('/items', (req, res) => { app.get('/items', (req, res) => {
res.render('items', { render(res, 'items', {
context: 'items', context: 'items',
items: items.items, items: items.items,
}); });

View File

@ -49,7 +49,7 @@ table.sticky-header {
table.sticky-header tr.header th { table.sticky-header tr.header th {
position: sticky; position: sticky;
top: 37px; top: 44px;
background-color: #fcfac8; background-color: #fcfac8;
z-index: 2; z-index: 2;
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); 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 { th.sorted, td.sorted {
background-color: #dae5f6 !important; background-color: #dae5f6 !important;
} }
table.row-clickable tbody.data td {
cursor: pointer;
}

184
web/static/calc.js Normal file
View File

@ -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));

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

View File

@ -3,9 +3,28 @@
const $locationFilterForm = $('.location-filter-form'); const $locationFilterForm = $('.location-filter-form');
const $table = $('#main-table'); 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 = { window.saga = {
sortData: () => { sortData: () => {
console.log('sorting data');
const qs = new URLSearchParams(window.location.search); const qs = new URLSearchParams(window.location.search);
const col = qs.get('col'); const col = qs.get('col');
let dir = qs.get('dir'); let dir = qs.get('dir');
@ -158,6 +177,33 @@
}) })
.show(); .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 = () => { const checkLocations = () => {
@ -184,20 +230,44 @@
} catch (e) {} } 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(); checkLocations();
checkApprentices(); checkApprentices();
checkCharStats();
window.saga.sortData(); window.saga.sortData();
window.saga.filterApprentices(); window.saga.filterApprentices();
window.saga.filterLocations(); window.saga.filterLocations();
};
window.addEventListener('popstate', () => { window.addEventListener('popstate', update);
checkLocations();
checkApprentices();
window.saga.sortData();
window.saga.filterApprentices();
window.saga.filterLocations();
});
$('.sortable-links a').click(function(e) { $('.sortable-links a').click(function(e) {
e.preventDefault(); e.preventDefault();
@ -207,4 +277,187 @@
$apprenticeFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterApprentices); $apprenticeFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterApprentices);
$locationFilterForm.find('input[type="checkbox"]').on('change', window.saga.filterLocations); $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)); }(window));

84
web/static/spells.js Normal file
View File

@ -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));

View File

@ -1,30 +1,31 @@
extends master.pug extends master.pug
block tab-content 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 tr.header-above
th(colspan="2") th(colspan="2")
th(colspan="3").text-center.bg-info.text-light Reward th(colspan="2").text-center.bg-info.text-light Reward
th(colspan="7").text-center.bg-secondary.text-light Stats th(colspan="6").text-center.bg-secondary.text-light Stats
th(colspan="5").text-center.bg-dark.text-light Resistance th(colspan="5").text-center.bg-dark.text-light Resistance
th(colspan="2")
tr.header tr.header
th.align-middle Img th.align-middle Img
+sortHeader('Name', 'name') +sortHeader('Name', 'name')
+sortHeader('Gold', 'gold') +sortHeader('Gold', 'gold')
+sortHeader('Exp', 'exp') +sortHeader('Exp', 'exp')
th.align-middle Drops
+sortHeader('HP', 'hp') +sortHeader('HP', 'hp')
+sortHeader('MP', 'mp') +sortHeader('MP', 'mp')
+sortHeader('Power', 'power') +sortHeader('Power', 'power')
+sortHeader('Guard', 'guard') +sortHeader('Guard', 'guard')
+sortHeader('Magic', 'magic') +sortHeader('Magic', 'magic')
+sortHeader('Speed', 'speed') +sortHeader('Speed', 'speed')
th.align-middle Spells
+sortHeader('Fire', 'res-fire') +sortHeader('Fire', 'res-fire')
+sortHeader('Ice', 'res-ice') +sortHeader('Ice', 'res-ice')
+sortHeader('Thunder', 'res-thunder') +sortHeader('Thunder', 'res-thunder')
+sortHeader('Vacuum', 'res-vacuum') +sortHeader('Vacuum', 'res-vacuum')
+sortHeader('Debuff', 'res-debuff') +sortHeader('Debuff', 'res-debuff')
th.align-middle Spells
th.align-middle Drops
tbody.data: each enemy in enemies tbody.data: each enemy in enemies
tr( tr(
data-name=enemy.name data-name=enemy.name
@ -39,30 +40,128 @@ block tab-content
data-res-fire=enemy.resistance.fire data-res-fire=enemy.resistance.fire
data-res-ice=enemy.resistance.ice data-res-ice=enemy.resistance.ice
data-res-thunder=enemy.resistance.thunder data-res-thunder=enemy.resistance.thunder
data-res-vacuum=enemy.resistance.vacuum data-res-vacuum=(enemy.resistance.vacuum === null ? 100 : enemy.resistance.vacuum)
data-res-debuff=enemy.resistance.debuff data-res-debuff=(enemy.resistance.debuff === null ? 100 : enemy.resistance.debuff)
) )
td td
td: strong= enemy.name td: strong= enemy.name
td: code= enemy.gold td.text-right: code= enemy.gold
td: code= enemy.exp 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 td
+na(Object.keys(enemy.drops).length) +na(Object.keys(enemy.drops).length)
ul.list-horizontal: each rate, item in enemy.drops ul.list-horizontal: each rate, item in enemy.drops
li li
= item + ' (' + (rate * 100).toFixed(2) + '%)' = item + ' (' + (rate * 100).toFixed(2) + '%)'
td: code= enemy.hp
td: code= enemy.mp div#enemy-info-modal.modal(tabindex="-1")
td: code= enemy.power div.modal-dialog.modal-lg
td: code= enemy.guard div.modal-content
td: code= enemy.magic div.modal-header
td: code= enemy.speed 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 td
+na(enemy.spells.length) span.hp
ul.list-horizontal: each spell in enemy.spells | /
li= spell span.mp
td: code= enemy.resistance.fire th Fire
td: code= enemy.resistance.ice td.res-fire
td: code= enemy.resistance.thunder tr
td: code= enemy.resistance.vacuum th Power
td: code= enemy.resistance.debuff 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'))

View File

@ -37,7 +37,8 @@ html
label.form-check-label(for=id)= location label.form-check-label(for=id)= location
div.container-fluid div.container-fluid
ul.nav.nav-tabs.mt-4.px-4.position-sticky(style="top: 0; background-color: white; z-index: 1") 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="/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="/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="/items" class=(context === 'items' ? 'active' : '')) Items
@ -45,13 +46,70 @@ html
li.nav-item: a.nav-link(href="/armor" class=(context === 'armor' ? 'active' : '')) Armor 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="/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="/exp" class=(context === 'exp' ? 'active' : '')) Experience
li.nav-item: a.nav-link(href="/calc" class=(context === 'calc' ? 'active' : '')) Calculations div
button.btn.btn-secondary.btn-sm(data-toggle="modal" data-target="#char-stats-modal") Character stats&hellip;
div.tab-content div.tab-content
div.tab-pane.show.active.mt-2 div.tab-pane.show.active.mt-2
block tab-content 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/jquery.js")
script(src="/static/popper.js") script(src="/static/popper.js")
script(src="/static/bootstrap.js") script(src="/static/bootstrap.js")
script(src="/static/js.cookie.js") script(src="/static/js.cookie.js")
script(src="/static/saga.js") script(src="/static/saga.js")
script(src="/static/calc.js")
script(src="/static/spells.js")

View File

@ -2,7 +2,7 @@ extends master.pug
block tab-content 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() +apprenticeFilterForm()
table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header table#main-table.table.table-sm.table-borderless.table-hover.table-striped.sticky-header
thead: tr.header thead: tr.header