feat(personajes): inventory capacity for shots per character
CI / Build Native (push) Successful in 6m57s

Add 2 new fields per character:
- inventorySlots (default 80) — slots disponibles en el inventario
- stackLimit (default 9999) — límite por stack (config del server)

Capacidad por personaje = slots × stack limit (total shots que puede
cargar). Útil para responder 'tengo N shots, ¿me alcanzan los chars?'.

Backend:
- V4__character_inventory.sql migration adds the 2 columns with defaults
- Character entity + CharacterDto + CharacterIn records include them
- CharacterService.updateInventory() + CharacterResource PUT
  /api/characters/{id}/inventory endpoint for inline edits
- Validation: slots and stackLimit must be > 0

Frontend:
- Agregar personaje form: 2 nuevos inputs (Slots default 80, Stack default 9999)
- Mis personajes table: columnas Slots, Stack y Capacidad (shots),
  editables inline con @blur y @keydown.enter (mismo patrón que level)
- Nueva sección '💼 Capacidad de inventario de shots' con:
  - Total de la cuenta (verde, grande)
  - Sub-info: N personajes · M slots · stack máx X
  - Cards por personaje (slots × stack)
- Alpine helpers nuevos: capacityOf(), totalCapacity(), totalSlots(),
  maxStack(), updateInventory()

Verificado en navegador:
- Math correcta: 100×9999 + 100×99999 = 10.999.800 shots ✓
- Edición inline persiste via PUT /inventory
- Migration V4 aplicada limpiamente en el boot (4 migrations total)
This commit is contained in:
2026-08-23 11:15:49 -04:00
parent c109a59d3d
commit c11f2e5aac
8 changed files with 184 additions and 7 deletions
@@ -31,6 +31,12 @@ public class Character extends PanacheEntityBase {
@Column(name = "level", nullable = false)
public int level;
@Column(name = "inventory_slots", nullable = false)
public int inventorySlots = 80;
@Column(name = "stack_limit", nullable = false)
public int stackLimit = 9999;
@Column(name = "created_at", nullable = false)
public Instant createdAt;
}
@@ -11,5 +11,7 @@ public record CharacterDto(
String name,
String characterClass,
int level,
int inventorySlots,
int stackLimit,
Instant createdAt) {
}
}
@@ -6,7 +6,9 @@ import io.quarkus.runtime.annotations.RegisterForReflection;
public record CharacterIn(
String name,
String characterClass,
int level) {
int level,
Integer inventorySlots,
Integer stackLimit) {
public CharacterIn {
if (name == null || name.isBlank()) {
@@ -25,4 +27,12 @@ public record CharacterIn(
throw new IllegalArgumentException("level must be greater than 0");
}
}
}
public int effectiveInventorySlots() {
return inventorySlots != null && inventorySlots > 0 ? inventorySlots : 80;
}
public int effectiveStackLimit() {
return stackLimit != null && stackLimit > 0 ? stackLimit : 9999;
}
}
@@ -52,7 +52,9 @@ public class CharacterResource {
CharacterIn validated = new CharacterIn(
input.name(),
input.characterClass(),
input.level());
input.level(),
input.inventorySlots(),
input.stackLimit());
CharacterDto created = characterService.create(userId.get(), validated);
return Response.status(201).entity(created).build();
} catch (IllegalArgumentException e) {
@@ -84,6 +86,30 @@ public class CharacterResource {
.orElse(Response.status(404).build());
}
@PUT
@Path("/{id}/inventory")
public Response updateInventory(@Context HttpHeaders headers,
@PathParam("id") String idStr,
UpdateInventoryRequest body) {
Optional<UUID> userId = extractUserId(headers);
if (userId.isEmpty()) return Response.status(401).build();
UUID id;
try {
id = UUID.fromString(idStr);
} catch (IllegalArgumentException e) {
return Response.status(400).entity("{\"error\":\"id inválido\"}").build();
}
if (body == null || body.inventorySlots() <= 0 || body.stackLimit() <= 0) {
return Response.status(400).entity("{\"error\":\"slots y stackLimit deben ser mayores a 0\"}").build();
}
Optional<CharacterDto> updated = characterService.updateInventory(userId.get(), id, body.inventorySlots(), body.stackLimit());
return updated.map(c -> Response.ok(c).build())
.orElse(Response.status(404).build());
}
@DELETE
@Path("/{id}")
public Response delete(@Context HttpHeaders headers, @PathParam("id") String idStr) {
@@ -116,4 +142,6 @@ public class CharacterResource {
}
public record UpdateLevelRequest(int level) {}
public record UpdateInventoryRequest(int inventorySlots, int stackLimit) {}
}
@@ -33,6 +33,8 @@ public class CharacterService {
entity.name = input.name();
entity.characterClass = input.characterClass();
entity.level = input.level();
entity.inventorySlots = input.effectiveInventorySlots();
entity.stackLimit = input.effectiveStackLimit();
entity.createdAt = Instant.now();
entity.persist();
return toDto(entity);
@@ -46,6 +48,15 @@ public class CharacterService {
return Optional.of(toDto(entity));
}
@Transactional
public Optional<CharacterDto> updateInventory(UUID userId, UUID id, int inventorySlots, int stackLimit) {
Character entity = Character.find("id = ?1 AND userId = ?2", id, userId).firstResult();
if (entity == null) return Optional.empty();
if (inventorySlots > 0) entity.inventorySlots = inventorySlots;
if (stackLimit > 0) entity.stackLimit = stackLimit;
return Optional.of(toDto(entity));
}
@Transactional
public void delete(UUID userId, UUID id) {
Character entity = Character.find("id = ?1 AND userId = ?2", id, userId).firstResult();
@@ -67,6 +78,8 @@ public class CharacterService {
entity.name,
entity.characterClass,
entity.level,
entity.inventorySlots,
entity.stackLimit,
entity.createdAt);
}
}
}