Files
shot-crafter-calculator/src/frontend/src/utils/calc.ts
T
darroyo 9c9fb3a2ca feat: shot-crafter-calculator with H2 persistence and production history
Quarkus 3.20.1 monolith serving React 18 + TypeScript + Tailwind SPA.

Features:
- Three-tab calculator (Insumos, Fórmulas, Calculadora) for
  Soulshot, Spiritshot and Blessed Spiritshot crafting in Lineage 2
  Interlude/Clásico with all 15 grades and pre-loaded recipes
- Real-time profitability computation (cristales → ore →
  crafteos → shots → cost → sale → ganancia)
- Multi-user auth with JWT in httpOnly cookie (bcrypt + RSA 2048)
- H2 file-based persistence in ./data/shots.mv.db (file-based, H2)
- Auto-save on state changes (debounced 500ms)
- Production history with stats (total/avg/best/worst/last5avg)
  and per-run detail modal with snapshot of insumos+formulas

Stack:
- Backend: Quarkus REST + Hibernate ORM Panache + smallrye-jwt
- Frontend: React 18 + TypeScript + Vite + Tailwind 3
- Build: Maven runs frontend-maven-plugin (Node 22 + npm ci)
  then copies dist to META-INF/resources for Quarkus to serve

Verified:
- 5 backend endpoints + 5 history endpoints with curl
- 35/35 browser tests via Playwright + Chromium
- All TS strict, all builds green
2026-08-12 16:03:51 -04:00

84 lines
2.4 KiB
TypeScript

import type { Calculo, Formula, Insumos, OreType, Totales } from '../types'
export function calcularFila(
formula: Formula,
cristalesDisponibles: number,
insumos: Insumos,
): Calculo {
const orePerCraft = formula.soulOreReq ?? formula.spiritOreReq ?? 0
const oreLabel: OreType | null =
formula.soulOreReq != null ? 'Soul Ore' : formula.spiritOreReq != null ? 'Spirit Ore' : null
if (formula.cristalesReq <= 0) {
return {
tipo: formula.tipo,
grado: formula.grado,
cristalesDisponibles,
cristalesUsados: 0,
oreNecesario: 0,
oreLabel,
crafteosPosibles: 0,
shotsObtenidos: 0,
costoTotal: 0,
valorVenta: 0,
ganancia: 0,
warning: 'Completa la receta en la pestaña Fórmulas',
}
}
const crafteosPosibles = Math.floor(cristalesDisponibles / formula.cristalesReq)
const cristalesUsados = crafteosPosibles * formula.cristalesReq
const oreNecesario = crafteosPosibles * orePerCraft
const shotsObtenidos = crafteosPosibles * formula.shotsObtenidos
const precioCristal = insumos.cristales[formula.grado]
const precioOre = orePerCraft > 0
? formula.soulOreReq != null
? insumos.soulOre
: insumos.spiritOre
: 0
const precioVenta = insumos.venta[formula.tipo][formula.grado]
const costoTotal = cristalesUsados * precioCristal + oreNecesario * precioOre
const valorVenta = shotsObtenidos * precioVenta
const ganancia = valorVenta - costoTotal
return {
tipo: formula.tipo,
grado: formula.grado,
cristalesDisponibles,
cristalesUsados,
oreNecesario,
oreLabel,
crafteosPosibles,
shotsObtenidos,
costoTotal,
valorVenta,
ganancia,
warning: null,
}
}
export function sumarTotales(calculos: Calculo[]): Totales {
return calculos.reduce<Totales>(
(acc, c) => ({
cristalesUsados: acc.cristalesUsados + c.cristalesUsados,
oreNecesario: acc.oreNecesario + c.oreNecesario,
crafteosPosibles: acc.crafteosPosibles + c.crafteosPosibles,
shotsObtenidos: acc.shotsObtenidos + c.shotsObtenidos,
costoTotal: acc.costoTotal + c.costoTotal,
valorVenta: acc.valorVenta + c.valorVenta,
ganancia: acc.ganancia + c.ganancia,
}),
{
cristalesUsados: 0,
oreNecesario: 0,
crafteosPosibles: 0,
shotsObtenidos: 0,
costoTotal: 0,
valorVenta: 0,
ganancia: 0,
},
)
}