diff --git a/core/src/main/java/com/loremind/application/campaigncontext/EnemyService.java b/core/src/main/java/com/loremind/application/campaigncontext/EnemyService.java index 50a43dc..a7f2ad6 100644 --- a/core/src/main/java/com/loremind/application/campaigncontext/EnemyService.java +++ b/core/src/main/java/com/loremind/application/campaigncontext/EnemyService.java @@ -2,8 +2,14 @@ package com.loremind.application.campaigncontext; import com.loremind.domain.campaigncontext.Enemy; import com.loremind.domain.campaigncontext.ports.EnemyRepository; +import com.loremind.domain.images.Image; +import com.loremind.application.images.ImageService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import java.io.ByteArrayInputStream; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,10 +22,14 @@ import java.util.Optional; @Service public class EnemyService { - private final EnemyRepository enemyRepository; + private static final Logger log = LoggerFactory.getLogger(EnemyService.class); - public EnemyService(EnemyRepository enemyRepository) { + private final EnemyRepository enemyRepository; + private final ImageService imageService; + + public EnemyService(EnemyRepository enemyRepository, ImageService imageService) { this.enemyRepository = enemyRepository; + this.imageService = imageService; } public record EnemyData( @@ -83,8 +93,12 @@ public class EnemyService { // --- Import de monstres depuis un compendium Foundry ------------------- - /** Un monstre du catalogue Foundry : nom + référence + snapshot de stats + dossier. */ - public record MonsterImport(String name, String foundryRef, Map stats, String folder) {} + /** + * Un monstre du catalogue Foundry : nom + référence + snapshot de stats + dossier + * + vignette du portrait (data URL base64, optionnelle). + */ + public record MonsterImport(String name, String foundryRef, Map stats, + String folder, String imgData) {} /** * Dossier LoreMind d'un monstre importé : on conserve l'arborescence Foundry @@ -95,6 +109,38 @@ public class EnemyService { return p.isEmpty() ? "Foundry" : "Foundry/" + p; } + /** + * Décode une vignette de portrait (data URL base64 "data:image/webp;base64,…") + * et l'enregistre comme image LoreMind. Renvoie l'id de l'image, ou null si + * absente/illisible (l'import ne doit jamais échouer à cause d'un portrait). + */ + private String storePortrait(String dataUrl, String name) { + if (dataUrl == null || !dataUrl.startsWith("data:")) return null; + int comma = dataUrl.indexOf(','); + if (comma < 0) return null; + String header = dataUrl.substring(5, comma); // "image/webp;base64" + String contentType = header.split(";")[0].toLowerCase(); + byte[] bytes; + try { + bytes = Base64.getDecoder().decode(dataUrl.substring(comma + 1)); + } catch (IllegalArgumentException e) { + return null; + } + if (bytes.length == 0) return null; + String ext = contentType.contains("png") ? "png" + : contentType.contains("jpeg") || contentType.contains("jpg") ? "jpg" + : contentType.contains("gif") ? "gif" : "webp"; + String filename = (name == null || name.isBlank() ? "monstre" : name.replaceAll("[^a-zA-Z0-9._-]", "_")) + "." + ext; + try { + Image img = imageService.upload(filename, contentType, + new ByteArrayInputStream(bytes), bytes.length); + return img.getId(); + } catch (RuntimeException e) { + log.warn("Portrait de monstre ignoré (\"{}\") : {}", name, e.getMessage()); + return null; + } + } + public record MonsterImportResult(int created, int updated) {} /** @@ -124,6 +170,11 @@ public class EnemyService { ex.setName(m.name()); ex.setFoundryStats(stats); // rafraîchit le snapshot ex.setFolder(folder); // ré-aligne sur l'arborescence Foundry + // Portrait : seulement s'il manque (pas de ré-upload à chaque import). + if (ex.getPortraitImageId() == null) { + String portrait = storePortrait(m.imgData(), m.name()); + if (portrait != null) ex.setPortraitImageId(portrait); + } enemyRepository.save(ex); updated++; } else { @@ -132,6 +183,7 @@ public class EnemyService { .foundryRef(m.foundryRef()) .foundryStats(stats) .folder(folder) + .portraitImageId(storePortrait(m.imgData(), m.name())) .campaignId(campaignId) .order(order++) .values(new HashMap<>()) diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/FoundryMonsterImportController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/FoundryMonsterImportController.java index d969fcf..22c557c 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/controller/FoundryMonsterImportController.java +++ b/core/src/main/java/com/loremind/infrastructure/web/controller/FoundryMonsterImportController.java @@ -27,7 +27,7 @@ public class FoundryMonsterImportController { public record MonsterCatalog(String system, List monsters) {} public record MonsterEntry(String name, String uuid, String type, String img, - java.util.Map stats, String folder) {} + java.util.Map stats, String folder, String imgData) {} @PostMapping("/import-foundry-monsters") public ResponseEntity importMonsters( @@ -35,7 +35,7 @@ public class FoundryMonsterImportController { @RequestBody MonsterCatalog catalog) { List monsters = (catalog.monsters() == null ? List.of() : catalog.monsters()) .stream() - .map(m -> new EnemyService.MonsterImport(m.name(), m.uuid(), m.stats(), m.folder())) + .map(m -> new EnemyService.MonsterImport(m.name(), m.uuid(), m.stats(), m.folder(), m.imgData())) .toList(); return ResponseEntity.ok(enemyService.importFoundryMonsters(campaignId, monsters)); } diff --git a/core/src/test/java/com/loremind/application/campaigncontext/EnemyServiceMonsterImportTest.java b/core/src/test/java/com/loremind/application/campaigncontext/EnemyServiceMonsterImportTest.java index efe0de3..5884668 100644 --- a/core/src/test/java/com/loremind/application/campaigncontext/EnemyServiceMonsterImportTest.java +++ b/core/src/test/java/com/loremind/application/campaigncontext/EnemyServiceMonsterImportTest.java @@ -30,8 +30,8 @@ class EnemyServiceMonsterImportTest { CampaignJpaEntity.builder().name("Camp").arcsCount(0).build()).getId(); var r1 = enemyService.importFoundryMonsters(String.valueOf(cid), List.of( - new EnemyService.MonsterImport("Goblin", "Compendium.nimble.monsters.Actor.g1", Map.of("level", "1"), "Briarban"), - new EnemyService.MonsterImport("Orc", "Compendium.nimble.monsters.Actor.o1", Map.of(), null))); + new EnemyService.MonsterImport("Goblin", "Compendium.nimble.monsters.Actor.g1", Map.of("level", "1"), "Briarban", null), + new EnemyService.MonsterImport("Orc", "Compendium.nimble.monsters.Actor.o1", Map.of(), null, null))); assertEquals(2, r1.created()); assertEquals(0, r1.updated()); assertEquals(2, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size()); @@ -49,8 +49,8 @@ class EnemyServiceMonsterImportTest { // Réimport : g1 déjà connu (renommé + redossiérisé) + un nouveau (k1). Pas de doublon. var r2 = enemyService.importFoundryMonsters(String.valueOf(cid), List.of( - new EnemyService.MonsterImport("Goblin Boss", "Compendium.nimble.monsters.Actor.g1", Map.of(), "Briarban/Bosses"), - new EnemyService.MonsterImport("Kobold", "Compendium.nimble.monsters.Actor.k1", Map.of(), "Kobolds"))); + new EnemyService.MonsterImport("Goblin Boss", "Compendium.nimble.monsters.Actor.g1", Map.of(), "Briarban/Bosses", null), + new EnemyService.MonsterImport("Kobold", "Compendium.nimble.monsters.Actor.k1", Map.of(), "Kobolds", null))); assertEquals(1, r2.created()); assertEquals(1, r2.updated()); assertEquals("Foundry/Briarban/Bosses", enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream() @@ -70,9 +70,9 @@ class EnemyServiceMonsterImportTest { CampaignJpaEntity.builder().name("Camp2").arcsCount(0).build()).getId(); var r = enemyService.importFoundryMonsters(String.valueOf(cid), List.of( - new EnemyService.MonsterImport("", "Compendium.x.Actor.a", Map.of(), null), - new EnemyService.MonsterImport("SansRef", " ", Map.of(), null), - new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b", Map.of(), null))); + new EnemyService.MonsterImport("", "Compendium.x.Actor.a", Map.of(), null, null), + new EnemyService.MonsterImport("SansRef", " ", Map.of(), null, null), + new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b", Map.of(), null, null))); assertEquals(1, r.created()); assertEquals(1, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size()); }