From a654020655c88321339b7b75ebd957e055fed3f2 Mon Sep 17 00:00:00 2001
From: Daniel Arroyo
Date: Sun, 23 Aug 2026 18:26:42 -0400
Subject: [PATCH] feat(ventas): structured item selector with weight
calculation and inventory usage
---
.../META-INF/resources/static/js/shots.js | 79 +++++++
.../templates/PageResource/personajes.html | 35 ++-
.../templates/PageResource/ventas.html | 199 ++++++++++++++++--
3 files changed, 271 insertions(+), 42 deletions(-)
create mode 100644 src/main/resources/META-INF/resources/static/js/shots.js
diff --git a/src/main/resources/META-INF/resources/static/js/shots.js b/src/main/resources/META-INF/resources/static/js/shots.js
new file mode 100644
index 0000000..f3c65b9
--- /dev/null
+++ b/src/main/resources/META-INF/resources/static/js/shots.js
@@ -0,0 +1,79 @@
+window.shots = {
+ WEIGHTS: {
+ Soulshot: { D: 3, C: 3, B: 2, A: 2, S: 2 },
+ Spiritshot: { D: 5, C: 3, B: 3, A: 2, S: 2 },
+ 'Blessed Spiritshot': { D: 5, C: 3, B: 3, A: 2, S: 2 },
+ },
+ TIPOS: ['Soulshot', 'Spiritshot', 'Blessed Spiritshot'],
+ GRADOS: ['D', 'C', 'B', 'A', 'S'],
+
+ itemWeight(tipo, grado) {
+ return this.WEIGHTS[tipo]?.[grado] ?? 0;
+ },
+
+ maxWeight(c) {
+ return (c.baseWeight || 115230) * (1 + (c.skillLevel || 0));
+ },
+
+ capacityOf(c, tipo, grado) {
+ const maxW = this.maxWeight(c);
+ const w = this.itemWeight(tipo, grado);
+ return w > 0 ? Math.floor(maxW / w) : 0;
+ },
+
+ totalCapacityOf(c) {
+ let total = 0;
+ for (const t of this.TIPOS) {
+ for (const g of this.GRADOS) {
+ total += this.capacityOf(c, t, g);
+ }
+ }
+ return total;
+ },
+
+ tipoShort(tipo) {
+ return { Soulshot: 'SS', Spiritshot: 'SPS', 'Blessed Spiritshot': 'BSS' }[tipo] || tipo;
+ },
+
+ calcRowWeight(tipo, grado, cantidad) {
+ return this.itemWeight(tipo, grado) * cantidad;
+ },
+
+ calcTotalWeight(items) {
+ return items.reduce((sum, item) => sum + this.calcRowWeight(item.tipo, item.grado, item.cantidad || 0), 0);
+ },
+
+ calcInventoryUsage(character, items) {
+ const maxW = this.maxWeight(character);
+ const totalW = this.calcTotalWeight(items);
+ return {
+ total: totalW,
+ max: maxW,
+ pct: maxW > 0 ? Math.round((totalW / maxW) * 100) : 0,
+ };
+ },
+
+ formatItemsDescription(items) {
+ return items
+ .filter(i => i.cantidad > 0 && i.tipo && i.grado)
+ .map(i => `${this.tipoShort(i.tipo)} ${i.grado} × ${Number(i.cantidad).toLocaleString('es-CL')}`)
+ .join('\n');
+ },
+
+ parseItemsDescription(text) {
+ if (!text) return [];
+ const lines = text.trim().split('\n');
+ const parsed = [];
+ for (const line of lines) {
+ const m = line.trim().match(/^(SS|SPS|BSS|\w+)\s+([DBCSA])\s*[×x]\s*([\d.,]+)/i);
+ if (!m) continue;
+ const tipoMap = { SS: 'Soulshot', SPS: 'Spiritshot', BSS: 'Blessed Spiritshot' };
+ const tipo = tipoMap[m[1]] || m[1];
+ const grado = m[2].toUpperCase();
+ const cantidad = parseInt(m[3].replace(/[.,]/g, ''), 10);
+ if (!this.WEIGHTS[tipo]?.[grado]) continue;
+ parsed.push({ uid: crypto.randomUUID(), tipo, grado, cantidad });
+ }
+ return parsed;
+ },
+};
diff --git a/src/main/resources/templates/PageResource/personajes.html b/src/main/resources/templates/PageResource/personajes.html
index 2fdeed8..6f904ed 100644
--- a/src/main/resources/templates/PageResource/personajes.html
+++ b/src/main/resources/templates/PageResource/personajes.html
@@ -216,10 +216,11 @@
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+