Possibilité d'importer un template de monstre depuis le systeme qu'on utilise sur Foundry pour forcer le template. Lors de l'export, le monstre est automatiquement adapté comme ça.
All checks were successful
All checks were successful
à l'import des monstres depuis foundry : on garde la structure originelle utilisée sur foundry également
This commit is contained in:
@@ -83,8 +83,17 @@ public class EnemyService {
|
||||
|
||||
// --- Import de monstres depuis un compendium Foundry -------------------
|
||||
|
||||
/** Un monstre du catalogue Foundry : nom + référence (UUID de compendium). */
|
||||
public record MonsterImport(String name, String foundryRef) {}
|
||||
/** Un monstre du catalogue Foundry : nom + référence + snapshot de stats + dossier. */
|
||||
public record MonsterImport(String name, String foundryRef, Map<String, String> stats, String folder) {}
|
||||
|
||||
/**
|
||||
* Dossier LoreMind d'un monstre importé : on conserve l'arborescence Foundry
|
||||
* sous un dossier racine « Foundry » ("Foundry/Briarban", "Foundry" si aucun).
|
||||
*/
|
||||
private static String foundryFolder(String path) {
|
||||
String p = path == null ? "" : path.trim();
|
||||
return p.isEmpty() ? "Foundry" : "Foundry/" + p;
|
||||
}
|
||||
|
||||
public record MonsterImportResult(int created, int updated) {}
|
||||
|
||||
@@ -108,15 +117,21 @@ public class EnemyService {
|
||||
|| m.name() == null || m.name().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> stats = m.stats() != null ? new HashMap<>(m.stats()) : new HashMap<>();
|
||||
String folder = foundryFolder(m.folder());
|
||||
Enemy ex = byRef.get(m.foundryRef());
|
||||
if (ex != null) {
|
||||
ex.setName(m.name());
|
||||
ex.setFoundryStats(stats); // rafraîchit le snapshot
|
||||
ex.setFolder(folder); // ré-aligne sur l'arborescence Foundry
|
||||
enemyRepository.save(ex);
|
||||
updated++;
|
||||
} else {
|
||||
Enemy saved = enemyRepository.save(Enemy.builder()
|
||||
.name(m.name())
|
||||
.foundryRef(m.foundryRef())
|
||||
.foundryStats(stats)
|
||||
.folder(folder)
|
||||
.campaignId(campaignId)
|
||||
.order(order++)
|
||||
.values(new HashMap<>())
|
||||
|
||||
@@ -4,11 +4,15 @@ import com.loremind.domain.gamesystemcontext.GameSystem;
|
||||
import com.loremind.domain.gamesystemcontext.RulesImportResult;
|
||||
import com.loremind.domain.gamesystemcontext.ports.GameSystemRepository;
|
||||
import com.loremind.domain.gamesystemcontext.ports.RulesPdfImporter;
|
||||
import com.loremind.domain.shared.template.FieldType;
|
||||
import com.loremind.domain.shared.template.TemplateField;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class GameSystemService {
|
||||
@@ -58,6 +62,7 @@ public class GameSystemService {
|
||||
List<TemplateField> characterTemplate,
|
||||
List<TemplateField> npcTemplate,
|
||||
List<TemplateField> enemyTemplate,
|
||||
String foundryActorType,
|
||||
String author,
|
||||
boolean isPublic
|
||||
) {}
|
||||
@@ -67,6 +72,7 @@ public class GameSystemService {
|
||||
.name(data.name())
|
||||
.description(data.description())
|
||||
.rulesMarkdown(data.rulesMarkdown())
|
||||
.foundryActorType(normalize(data.foundryActorType()))
|
||||
.author(normalize(data.author()))
|
||||
.isPublic(data.isPublic())
|
||||
.build();
|
||||
@@ -93,11 +99,41 @@ public class GameSystemService {
|
||||
existing.replaceCharacterTemplate(data.characterTemplate());
|
||||
existing.replaceNpcTemplate(data.npcTemplate());
|
||||
existing.replaceEnemyTemplate(data.enemyTemplate());
|
||||
existing.setFoundryActorType(normalize(data.foundryActorType()));
|
||||
existing.setAuthor(normalize(data.author()));
|
||||
existing.setPublic(data.isPublic());
|
||||
return gameSystemRepository.save(existing);
|
||||
}
|
||||
|
||||
/** Un champ scalaire d'une structure d'acteur Foundry importée. */
|
||||
public record FoundryStructField(String path, String label, String type) {}
|
||||
|
||||
/**
|
||||
* Remplace le template ENNEMI par une structure importée d'un système Foundry :
|
||||
* chaque champ devient un TemplateField mappé à son chemin Foundry. Pose aussi le
|
||||
* type d'acteur. L'utilisateur élague/renomme ensuite dans l'éditeur de template.
|
||||
*/
|
||||
public GameSystem importFoundryStructure(String id, String actorType, List<FoundryStructField> fields) {
|
||||
GameSystem gs = gameSystemRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("GameSystem non trouvé avec l'ID: " + id));
|
||||
|
||||
List<TemplateField> template = new ArrayList<>();
|
||||
Set<String> usedNames = new HashSet<>();
|
||||
for (FoundryStructField f : fields == null ? List.<FoundryStructField>of() : fields) {
|
||||
if (f.path() == null || f.path().isBlank()) continue;
|
||||
String label = (f.label() != null && !f.label().isBlank()) ? f.label().trim() : f.path();
|
||||
// Nom unique : libellé si libre, sinon le chemin (toujours unique).
|
||||
String name = usedNames.contains(label.toLowerCase()) ? f.path() : label;
|
||||
if (usedNames.contains(name.toLowerCase())) continue;
|
||||
usedNames.add(name.toLowerCase());
|
||||
FieldType type = "number".equalsIgnoreCase(f.type()) ? FieldType.NUMBER : FieldType.TEXT;
|
||||
template.add(new TemplateField(name, type, null, null, f.path()));
|
||||
}
|
||||
gs.replaceEnemyTemplate(template);
|
||||
gs.setFoundryActorType(normalize(actorType));
|
||||
return gameSystemRepository.save(gs);
|
||||
}
|
||||
|
||||
public void deleteGameSystem(String id) {
|
||||
gameSystemRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ public class Enemy {
|
||||
*/
|
||||
private String foundryRef;
|
||||
|
||||
/**
|
||||
* Instantané (figé) des stats de l'acteur Foundry d'origine, aplati en
|
||||
* cle->valeur, pour AFFICHAGE en prep dans LoreMind. Dependant du systeme,
|
||||
* non synchronise (les stats vivantes restent cote Foundry). Jamais null apres
|
||||
* construction.
|
||||
*/
|
||||
private Map<String, String> foundryStats;
|
||||
|
||||
/** Ordre d'affichage dans la liste. */
|
||||
private int order;
|
||||
|
||||
@@ -75,4 +83,9 @@ public class Enemy {
|
||||
if (keyValueValues == null) keyValueValues = new HashMap<>();
|
||||
return keyValueValues;
|
||||
}
|
||||
|
||||
public Map<String, String> getFoundryStats() {
|
||||
if (foundryStats == null) foundryStats = new HashMap<>();
|
||||
return foundryStats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,13 @@ public class GameSystem {
|
||||
*/
|
||||
private List<TemplateField> enemyTemplate;
|
||||
|
||||
/**
|
||||
* Type d'acteur Foundry à créer pour les ennemis maison de ce système
|
||||
* (ex: {@code npc}, {@code monster}). Renseigné par l'import de structure Foundry ;
|
||||
* couplé aux {@code foundryPath} des champs de {@link #enemyTemplate}. Nullable.
|
||||
*/
|
||||
private String foundryActorType;
|
||||
|
||||
/** Auteur déclaré — futur marketplace. Nullable. */
|
||||
private String author;
|
||||
|
||||
|
||||
@@ -37,14 +37,26 @@ public class TemplateField {
|
||||
*/
|
||||
private List<String> labels;
|
||||
|
||||
/**
|
||||
* Chemin Foundry de ce champ (ex: {@code attributes.hp.value}) quand le template
|
||||
* est calqué sur un système Foundry importé. Permet, a l'export, de construire un
|
||||
* acteur typé en posant {@code system.<foundryPath> = valeur}. Null = non mappé.
|
||||
*/
|
||||
private String foundryPath;
|
||||
|
||||
/** Constructeur de retrocompat : type seul, layout/labels=null. */
|
||||
public TemplateField(String name, FieldType type) {
|
||||
this(name, type, null, null);
|
||||
this(name, type, null, null, null);
|
||||
}
|
||||
|
||||
/** Constructeur de retrocompat : type + layout, labels=null. */
|
||||
public TemplateField(String name, FieldType type, ImageLayout layout) {
|
||||
this(name, type, layout, null);
|
||||
this(name, type, layout, null, null);
|
||||
}
|
||||
|
||||
/** Constructeur de retrocompat (4 args) : sans foundryPath. */
|
||||
public TemplateField(String name, FieldType type, ImageLayout layout, List<String> labels) {
|
||||
this(name, type, layout, labels, null);
|
||||
}
|
||||
|
||||
/** Raccourci : construit un champ de type TEXT (cas le plus courant). */
|
||||
|
||||
@@ -67,6 +67,11 @@ public class EnemyJpaEntity {
|
||||
@Column(name = "foundry_ref", length = 512)
|
||||
private String foundryRef;
|
||||
|
||||
/** Instantané figé des stats Foundry (aplati cle->valeur), JSON en TEXT. */
|
||||
@Convert(converter = StringMapJsonConverter.class)
|
||||
@Column(name = "foundry_stats", columnDefinition = "TEXT")
|
||||
private Map<String, String> foundryStats;
|
||||
|
||||
@Column(name = "\"order\"", nullable = false)
|
||||
private int order;
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ public class GameSystemJpaEntity {
|
||||
@Column(name = "enemy_template", columnDefinition = "TEXT")
|
||||
private List<TemplateField> enemyTemplate;
|
||||
|
||||
/** Type d'acteur Foundry pour les ennemis maison (import de structure). Nullable. */
|
||||
@Column(name = "foundry_actor_type")
|
||||
private String foundryActorType;
|
||||
|
||||
@Column
|
||||
private String author;
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ public class PostgresEnemyRepository implements EnemyRepository {
|
||||
.keyValueValues(e.getKeyValueValues() != null ? new HashMap<>(e.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(e.getCampaignId().toString())
|
||||
.foundryRef(e.getFoundryRef())
|
||||
.foundryStats(e.getFoundryStats() != null ? new HashMap<>(e.getFoundryStats()) : new HashMap<>())
|
||||
.order(e.getOrder())
|
||||
.createdAt(e.getCreatedAt())
|
||||
.updatedAt(e.getUpdatedAt())
|
||||
@@ -81,6 +82,7 @@ public class PostgresEnemyRepository implements EnemyRepository {
|
||||
.keyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>())
|
||||
.campaignId(Long.parseLong(n.getCampaignId()))
|
||||
.foundryRef(n.getFoundryRef())
|
||||
.foundryStats(n.getFoundryStats() != null ? new HashMap<>(n.getFoundryStats()) : new HashMap<>())
|
||||
.order(n.getOrder())
|
||||
.createdAt(n.getCreatedAt())
|
||||
.updatedAt(n.getUpdatedAt())
|
||||
|
||||
@@ -70,6 +70,7 @@ public class PostgresGameSystemRepository implements GameSystemRepository {
|
||||
.enemyTemplate(e.getEnemyTemplate() != null
|
||||
? new java.util.ArrayList<>(e.getEnemyTemplate())
|
||||
: new java.util.ArrayList<>())
|
||||
.foundryActorType(e.getFoundryActorType())
|
||||
.author(e.getAuthor())
|
||||
.isPublic(e.isPublic())
|
||||
.createdAt(e.getCreatedAt())
|
||||
@@ -93,6 +94,7 @@ public class PostgresGameSystemRepository implements GameSystemRepository {
|
||||
.enemyTemplate(g.getEnemyTemplate() != null
|
||||
? new java.util.ArrayList<>(g.getEnemyTemplate())
|
||||
: new java.util.ArrayList<>())
|
||||
.foundryActorType(g.getFoundryActorType())
|
||||
.author(g.getAuthor())
|
||||
.isPublic(g.isPublic())
|
||||
.createdAt(g.getCreatedAt())
|
||||
|
||||
@@ -293,7 +293,7 @@ public class ExportService {
|
||||
private ContentExport.GameSystemDto toGameSystemDto(GameSystemJpaEntity e) {
|
||||
return new ContentExport.GameSystemDto(e.getId(), e.getName(), e.getDescription(),
|
||||
e.getRulesMarkdown(), e.getCharacterTemplate(), e.getNpcTemplate(),
|
||||
e.getEnemyTemplate(), e.getAuthor(), e.isPublic());
|
||||
e.getEnemyTemplate(), e.getFoundryActorType(), e.getAuthor(), e.isPublic());
|
||||
}
|
||||
|
||||
private ContentExport.LoreDto toLoreDto(LoreJpaEntity e) {
|
||||
@@ -364,7 +364,7 @@ public class ExportService {
|
||||
private ContentExport.EnemyDto toEnemyDto(EnemyJpaEntity e) {
|
||||
return new ContentExport.EnemyDto(e.getId(), e.getName(), e.getLevel(), e.getFolder(),
|
||||
e.getPortraitImageId(), e.getHeaderImageId(), e.getValues(), e.getImageValues(),
|
||||
e.getKeyValueValues(), e.getCampaignId(), e.getFoundryRef(), e.getOrder());
|
||||
e.getKeyValueValues(), e.getCampaignId(), e.getFoundryRef(), e.getFoundryStats(), e.getOrder());
|
||||
}
|
||||
|
||||
private ContentExport.ItemCatalogDto toItemCatalogDto(ItemCatalogJpaEntity e) {
|
||||
|
||||
@@ -136,6 +136,7 @@ public class ImportService {
|
||||
e.setCharacterTemplate(d.characterTemplate());
|
||||
e.setNpcTemplate(d.npcTemplate());
|
||||
e.setEnemyTemplate(d.enemyTemplate());
|
||||
e.setFoundryActorType(d.foundryActorType());
|
||||
e.setAuthor(d.author());
|
||||
e.setPublic(d.isPublic());
|
||||
gameSystemMap.put(d.id(), gameSystemRepo.save(e).getId());
|
||||
@@ -336,6 +337,7 @@ public class ImportService {
|
||||
e.setKeyValueValues(d.keyValueValues());
|
||||
e.setCampaignId(IdRemapper.remapId(campaignMap, d.campaignId()));
|
||||
e.setFoundryRef(d.foundryRef()); // ref externe Foundry : conservee telle quelle
|
||||
e.setFoundryStats(d.foundryStats());
|
||||
e.setOrder(d.order());
|
||||
enemyMap.put(d.id(), enemyRepo.save(e).getId());
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public record ContentExport(
|
||||
List<TemplateField> characterTemplate,
|
||||
List<TemplateField> npcTemplate,
|
||||
List<TemplateField> enemyTemplate,
|
||||
String foundryActorType,
|
||||
String author,
|
||||
boolean isPublic
|
||||
) {}
|
||||
@@ -210,6 +211,7 @@ public record ContentExport(
|
||||
Map<String, Map<String, String>> keyValueValues,
|
||||
Long campaignId,
|
||||
String foundryRef,
|
||||
Map<String, String> foundryStats,
|
||||
int order
|
||||
) {}
|
||||
|
||||
|
||||
@@ -77,9 +77,15 @@ public final class FoundryBundle {
|
||||
public record Persona(
|
||||
String id, String name, String folder, int order,
|
||||
String portraitAssetId, String headerAssetId, String level,
|
||||
String foundryRef, List<Field> fields
|
||||
String foundryRef, FoundryActor foundryActor, List<Field> fields
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Acteur Foundry typé à créer pour un ennemi MAISON (sans référence) : type
|
||||
* d'acteur + données system pré-construites depuis les champs mappés du template.
|
||||
*/
|
||||
public record FoundryActor(String type, Map<String, Object> system) {}
|
||||
|
||||
/** Champ de fiche resolu : {type, label} + selon le type value | entries | assetIds. */
|
||||
public record Field(String type, String label, String value, List<Entry> entries, List<String> assetIds) {}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ public class FoundryExportService {
|
||||
|
||||
List<TemplateField> npcTemplate = resolveTemplate(campaign.getGameSystemId(), true);
|
||||
List<TemplateField> enemyTemplate = resolveTemplate(campaign.getGameSystemId(), false);
|
||||
String foundryActorType = resolveActorType(campaign.getGameSystemId());
|
||||
|
||||
AssetRegistry assets = new AssetRegistry();
|
||||
|
||||
@@ -133,7 +134,7 @@ public class FoundryExportService {
|
||||
for (NpcJpaEntity n : npcRepo.findByCampaignIdOrderByOrderAsc(campaign.getId())) {
|
||||
npcs.add(new FoundryBundle.Persona(
|
||||
str(n.getId()), n.getName(), n.getFolder(), n.getOrder(),
|
||||
assets.image(n.getPortraitImageId()), assets.image(n.getHeaderImageId()), null, null,
|
||||
assets.image(n.getPortraitImageId()), assets.image(n.getHeaderImageId()), null, null, null,
|
||||
fields(npcTemplate, n.getValues(), n.getKeyValueValues(), n.getImageValues(), assets)));
|
||||
}
|
||||
|
||||
@@ -143,6 +144,7 @@ public class FoundryExportService {
|
||||
str(e.getId()), e.getName(), e.getFolder(), e.getOrder(),
|
||||
assets.image(e.getPortraitImageId()), assets.image(e.getHeaderImageId()), e.getLevel(),
|
||||
e.getFoundryRef(),
|
||||
buildFoundryActor(e, enemyTemplate, foundryActorType),
|
||||
fields(enemyTemplate, e.getValues(), e.getKeyValueValues(), e.getImageValues(), assets)));
|
||||
}
|
||||
|
||||
@@ -284,6 +286,65 @@ public class FoundryExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveActorType(String gameSystemId) {
|
||||
if (gameSystemId == null || gameSystemId.isBlank()) return null;
|
||||
try {
|
||||
return gameSystemRepo.findById(Long.parseLong(gameSystemId))
|
||||
.map(GameSystemJpaEntity::getFoundryActorType)
|
||||
.orElse(null);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit l'acteur Foundry typé d'un ennemi MAISON (sans référence) : pose
|
||||
* {@code system.<foundryPath> = valeur} pour chaque champ mappé non vide. Null si
|
||||
* pas de type d'acteur, pas de champ mappé, ou si l'ennemi a déjà une référence
|
||||
* (auquel cas le vrai acteur du compendium est utilisé à la place).
|
||||
*/
|
||||
private FoundryBundle.FoundryActor buildFoundryActor(EnemyJpaEntity e, List<TemplateField> template, String actorType) {
|
||||
if (actorType == null || actorType.isBlank() || template == null) return null;
|
||||
if (e.getFoundryRef() != null && !e.getFoundryRef().isBlank()) return null;
|
||||
Map<String, Object> system = new LinkedHashMap<>();
|
||||
Map<String, String> values = e.getValues();
|
||||
boolean any = false;
|
||||
for (TemplateField f : template) {
|
||||
if (f == null || f.getName() == null) continue;
|
||||
String path = f.getFoundryPath();
|
||||
if (path == null || path.isBlank()) continue;
|
||||
String raw = values != null ? values.get(f.getName()) : null;
|
||||
if (raw == null || raw.isBlank()) continue;
|
||||
setPath(system, path, f.getType() == FieldType.NUMBER ? parseNumber(raw) : raw);
|
||||
any = true;
|
||||
}
|
||||
return any ? new FoundryBundle.FoundryActor(actorType, system) : null;
|
||||
}
|
||||
|
||||
/** Pose une valeur dans un objet imbriqué selon un chemin pointé ("a.b.c"). */
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void setPath(Map<String, Object> root, String path, Object value) {
|
||||
String[] parts = path.split("\\.");
|
||||
Map<String, Object> cur = root;
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
Object next = cur.get(parts[i]);
|
||||
if (!(next instanceof Map)) {
|
||||
next = new LinkedHashMap<String, Object>();
|
||||
cur.put(parts[i], next);
|
||||
}
|
||||
cur = (Map<String, Object>) next;
|
||||
}
|
||||
cur.put(parts[parts.length - 1], value);
|
||||
}
|
||||
|
||||
/** Convertit en Integer ou Double si possible (pour que Foundry reçoive un nombre). */
|
||||
private static Object parseNumber(String s) {
|
||||
String t = s.trim();
|
||||
try { return Integer.valueOf(t); } catch (NumberFormatException ignored) { /* fallthrough */ }
|
||||
try { return Double.valueOf(t.replace(',', '.')); } catch (NumberFormatException ignored) { /* fallthrough */ }
|
||||
return s;
|
||||
}
|
||||
|
||||
private List<FoundryBundle.Field> fields(List<TemplateField> template,
|
||||
Map<String, String> values,
|
||||
Map<String, Map<String, String>> keyValueValues,
|
||||
|
||||
@@ -26,7 +26,8 @@ public class FoundryMonsterImportController {
|
||||
/** Format du catalogue produit par le module Foundry. */
|
||||
public record MonsterCatalog(String system, List<MonsterEntry> monsters) {}
|
||||
|
||||
public record MonsterEntry(String name, String uuid, String img) {}
|
||||
public record MonsterEntry(String name, String uuid, String type, String img,
|
||||
java.util.Map<String, String> stats, String folder) {}
|
||||
|
||||
@PostMapping("/import-foundry-monsters")
|
||||
public ResponseEntity<EnemyService.MonsterImportResult> importMonsters(
|
||||
@@ -34,7 +35,7 @@ public class FoundryMonsterImportController {
|
||||
@RequestBody MonsterCatalog catalog) {
|
||||
List<EnemyService.MonsterImport> monsters = (catalog.monsters() == null ? List.<MonsterEntry>of() : catalog.monsters())
|
||||
.stream()
|
||||
.map(m -> new EnemyService.MonsterImport(m.name(), m.uuid()))
|
||||
.map(m -> new EnemyService.MonsterImport(m.name(), m.uuid(), m.stats(), m.folder()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(enemyService.importFoundryMonsters(campaignId, monsters));
|
||||
}
|
||||
|
||||
@@ -261,8 +261,29 @@ public class GameSystemController {
|
||||
templateFieldMapper.toDomainList(dto.getCharacterTemplate()),
|
||||
templateFieldMapper.toDomainList(dto.getNpcTemplate()),
|
||||
templateFieldMapper.toDomainList(dto.getEnemyTemplate()),
|
||||
dto.getFoundryActorType(),
|
||||
dto.getAuthor(),
|
||||
dto.isPublic()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe une structure d'acteur Foundry (exportée par le module) : remplace le
|
||||
* template ENNEMI par les champs mappés + pose le type d'acteur. Renvoie le système
|
||||
* mis à jour (le front rafraîchit l'éditeur de template).
|
||||
*/
|
||||
@PostMapping("/{id}/import-foundry-structure")
|
||||
public ResponseEntity<GameSystemDTO> importFoundryStructure(
|
||||
@PathVariable String id, @RequestBody FoundryStructureRequest req) {
|
||||
List<GameSystemService.FoundryStructField> fields =
|
||||
(req.fields() == null ? List.<StructFieldDto>of() : req.fields()).stream()
|
||||
.map(f -> new GameSystemService.FoundryStructField(f.path(), f.label(), f.type()))
|
||||
.collect(Collectors.toList());
|
||||
GameSystem updated = gameSystemService.importFoundryStructure(id, req.actorType(), fields);
|
||||
return ResponseEntity.ok(gameSystemMapper.toDTO(updated));
|
||||
}
|
||||
|
||||
public record FoundryStructureRequest(String system, String actorType, List<StructFieldDto> fields) {}
|
||||
|
||||
public record StructFieldDto(String path, String label, String type) {}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ public class GameSystemDTO {
|
||||
private List<TemplateFieldDTO> characterTemplate = new ArrayList<>();
|
||||
private List<TemplateFieldDTO> npcTemplate = new ArrayList<>();
|
||||
private List<TemplateFieldDTO> enemyTemplate = new ArrayList<>();
|
||||
/** Type d'acteur Foundry pour l'export des ennemis maison (import de structure). */
|
||||
private String foundryActorType;
|
||||
private String author;
|
||||
private boolean isPublic;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,21 @@ public class TemplateFieldDTO {
|
||||
/** Labels predefinis pour KEY_VALUE_LIST (ordre significatif). */
|
||||
private List<String> labels;
|
||||
|
||||
/** Chemin Foundry du champ (mapping pour l'export d'acteur typé). Nullable. */
|
||||
private String foundryPath;
|
||||
|
||||
/** Retrocompat : constructeur sans foundryPath. */
|
||||
public TemplateFieldDTO(String name, String type, String layout, List<String> labels) {
|
||||
this(name, type, layout, labels, null);
|
||||
}
|
||||
|
||||
/** Retrocompat : constructeur sans labels. */
|
||||
public TemplateFieldDTO(String name, String type, String layout) {
|
||||
this(name, type, layout, null);
|
||||
this(name, type, layout, null, null);
|
||||
}
|
||||
|
||||
/** Retrocompat : constructeur sans layout ni labels. */
|
||||
public TemplateFieldDTO(String name, String type) {
|
||||
this(name, type, null, null);
|
||||
this(name, type, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public class GameSystemMapper {
|
||||
dto.setCharacterTemplate(fieldMapper.toDTOList(g.getCharacterTemplate()));
|
||||
dto.setNpcTemplate(fieldMapper.toDTOList(g.getNpcTemplate()));
|
||||
dto.setEnemyTemplate(fieldMapper.toDTOList(g.getEnemyTemplate()));
|
||||
dto.setFoundryActorType(g.getFoundryActorType());
|
||||
dto.setAuthor(g.getAuthor());
|
||||
dto.setPublic(g.isPublic());
|
||||
return dto;
|
||||
@@ -38,6 +39,7 @@ public class GameSystemMapper {
|
||||
.characterTemplate(fieldMapper.toDomainList(dto.getCharacterTemplate()))
|
||||
.npcTemplate(fieldMapper.toDomainList(dto.getNpcTemplate()))
|
||||
.enemyTemplate(fieldMapper.toDomainList(dto.getEnemyTemplate()))
|
||||
.foundryActorType(dto.getFoundryActorType())
|
||||
.author(dto.getAuthor())
|
||||
.isPublic(dto.isPublic())
|
||||
.build();
|
||||
|
||||
@@ -33,7 +33,7 @@ public class TemplateFieldMapper {
|
||||
&& field.getLabels() != null) {
|
||||
labels = new ArrayList<>(field.getLabels());
|
||||
}
|
||||
return new TemplateFieldDTO(field.getName(), typeStr, layoutStr, labels);
|
||||
return new TemplateFieldDTO(field.getName(), typeStr, layoutStr, labels, field.getFoundryPath());
|
||||
}
|
||||
|
||||
public TemplateField toDomain(TemplateFieldDTO dto) {
|
||||
@@ -58,7 +58,7 @@ public class TemplateFieldMapper {
|
||||
if ((type == FieldType.KEY_VALUE_LIST || type == FieldType.TABLE) && dto.getLabels() != null) {
|
||||
labels = new ArrayList<>(dto.getLabels());
|
||||
}
|
||||
return new TemplateField(dto.getName(), type, layout, labels);
|
||||
return new TemplateField(dto.getName(), type, layout, labels, dto.getFoundryPath());
|
||||
}
|
||||
|
||||
/** Mappe une liste de champs domaine → DTO ({@code null} → liste vide). */
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- ============================================================================
|
||||
-- V4 : instantané des stats Foundry sur les ennemis (affichage en prep).
|
||||
-- ============================================================================
|
||||
-- A l'import d'un monstre Foundry, on stocke un snapshot APLATI (cle->valeur) de
|
||||
-- ses stats systeme, juste pour l'affichage cote LoreMind. JSON en TEXT (meme
|
||||
-- mecanique que field_values). Nullable, figé (non synchronise avec Foundry).
|
||||
|
||||
alter table enemies add column foundry_stats TEXT;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- ============================================================================
|
||||
-- V5 : type d'acteur Foundry sur le GameSystem (export d'ennemis maison typés).
|
||||
-- ============================================================================
|
||||
-- Renseigne par l'import d'une structure d'acteur Foundry. Couple aux foundryPath
|
||||
-- des champs du template ennemi : a l'export, un ennemi maison devient un acteur de
|
||||
-- ce type avec system.<foundryPath> = valeur. Nullable.
|
||||
|
||||
alter table game_systems add column foundry_actor_type varchar(64);
|
||||
@@ -9,6 +9,7 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -29,18 +30,32 @@ 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"),
|
||||
new EnemyService.MonsterImport("Orc", "Compendium.nimble.monsters.Actor.o1")));
|
||||
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)));
|
||||
assertEquals(2, r1.created());
|
||||
assertEquals(0, r1.updated());
|
||||
assertEquals(2, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
||||
|
||||
// Réimport : g1 déjà connu (renommé) + un nouveau (k1). Pas de doublon pour g1.
|
||||
// Snapshot de stats conservé + arborescence Foundry sous "Foundry/".
|
||||
var goblin = enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream()
|
||||
.filter(e -> "Compendium.nimble.monsters.Actor.g1".equals(e.getFoundryRef()))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("1", goblin.getFoundryStats().get("level"));
|
||||
assertEquals("Foundry/Briarban", goblin.getFolder());
|
||||
var orc = enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream()
|
||||
.filter(e -> "Compendium.nimble.monsters.Actor.o1".equals(e.getFoundryRef()))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("Foundry", orc.getFolder()); // sans dossier Foundry -> racine
|
||||
|
||||
// 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"),
|
||||
new EnemyService.MonsterImport("Kobold", "Compendium.nimble.monsters.Actor.k1")));
|
||||
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")));
|
||||
assertEquals(1, r2.created());
|
||||
assertEquals(1, r2.updated());
|
||||
assertEquals("Foundry/Briarban/Bosses", enemyRepo.findByCampaignIdOrderByOrderAsc(cid).stream()
|
||||
.filter(e -> "Compendium.nimble.monsters.Actor.g1".equals(e.getFoundryRef()))
|
||||
.findFirst().orElseThrow().getFolder());
|
||||
|
||||
var all = enemyRepo.findByCampaignIdOrderByOrderAsc(cid);
|
||||
assertEquals(3, all.size());
|
||||
@@ -55,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"),
|
||||
new EnemyService.MonsterImport("SansRef", " "),
|
||||
new EnemyService.MonsterImport("Valide", "Compendium.x.Actor.b")));
|
||||
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)));
|
||||
assertEquals(1, r.created());
|
||||
assertEquals(1, enemyRepo.findByCampaignIdOrderByOrderAsc(cid).size());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.loremind.infrastructure.transfer.foundry;
|
||||
|
||||
import com.loremind.application.gamesystemcontext.GameSystemService;
|
||||
import com.loremind.domain.gamesystemcontext.GameSystem;
|
||||
import com.loremind.domain.shared.template.FieldType;
|
||||
import com.loremind.infrastructure.persistence.entity.*;
|
||||
import com.loremind.infrastructure.persistence.jpa.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Pont « structure système Foundry » : import d'une structure -> template ennemi
|
||||
* mappé (foundryPath) + type d'acteur, puis export d'un ennemi MAISON en acteur typé
|
||||
* ({@code system.<chemin> = valeur}).
|
||||
*/
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
class GameSystemStructureExportTest {
|
||||
|
||||
@Autowired private GameSystemService gameSystemService;
|
||||
@Autowired private GameSystemJpaRepository gameSystemRepo;
|
||||
@Autowired private FoundryExportService exportService;
|
||||
@Autowired private CampaignJpaRepository campaignRepo;
|
||||
@Autowired private EnemyJpaRepository enemyRepo;
|
||||
|
||||
@Test
|
||||
void importStructureThenExportTypedActor() {
|
||||
GameSystemJpaEntity gs = gameSystemRepo.save(
|
||||
GameSystemJpaEntity.builder().name("Nimble-like").isPublic(false).build());
|
||||
|
||||
// Import de structure : 2 champs scalaires mappés + type d'acteur "npc".
|
||||
GameSystem updated = gameSystemService.importFoundryStructure(String.valueOf(gs.getId()), "npc", List.of(
|
||||
new GameSystemService.FoundryStructField("attributes.hp.value", "PV", "number"),
|
||||
new GameSystemService.FoundryStructField("abilities.str.value", "FOR", "number")));
|
||||
|
||||
assertEquals("npc", updated.getFoundryActorType());
|
||||
assertEquals(2, updated.getEnemyTemplate().size());
|
||||
var pv = updated.getEnemyTemplate().stream().filter(f -> "PV".equals(f.getName())).findFirst().orElseThrow();
|
||||
assertEquals("attributes.hp.value", pv.getFoundryPath());
|
||||
assertEquals(FieldType.NUMBER, pv.getType());
|
||||
|
||||
// Campagne adossée au système + un ennemi MAISON (sans référence) rempli.
|
||||
CampaignJpaEntity camp = campaignRepo.save(CampaignJpaEntity.builder()
|
||||
.name("Camp").arcsCount(0).gameSystemId(String.valueOf(gs.getId())).build());
|
||||
enemyRepo.save(EnemyJpaEntity.builder()
|
||||
.campaignId(camp.getId()).name("Bandit maison").order(0)
|
||||
.values(Map.of("PV", "11", "FOR", "3"))
|
||||
.build());
|
||||
|
||||
FoundryExportService.BuiltBundle bundle =
|
||||
exportService.buildBundle(String.valueOf(camp.getId()), "2026-06-25T00:00:00Z");
|
||||
|
||||
var enemy = bundle.data().enemies().stream()
|
||||
.filter(e -> "Bandit maison".equals(e.name())).findFirst().orElseThrow();
|
||||
assertNotNull(enemy.foundryActor(), "un ennemi maison mappé doit porter un foundryActor");
|
||||
assertEquals("npc", enemy.foundryActor().type());
|
||||
|
||||
// system imbriqué : attributes.hp.value = 11, abilities.str.value = 3.
|
||||
Map<String, Object> system = enemy.foundryActor().system();
|
||||
assertEquals(11, nested(system, "attributes", "hp", "value"));
|
||||
assertEquals(3, nested(system, "abilities", "str", "value"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Object nested(Map<String, Object> root, String... path) {
|
||||
Object cur = root;
|
||||
for (String p : path) cur = ((Map<String, Object>) cur).get(p);
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user