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 path = require('path');
const {spells} = require('./spells');
const {spells} = require('../web/static/spells');
const spellMap = spells.reduce((map, 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 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,
});

View File

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

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

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
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'))

View File

@ -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&hellip;
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")

View File

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