Ajout de tableaux dans la partie templates / pages de lore : possibilité d'ajouter un tableau multiligne (par exemple pour faire des tableaux d'objets dans les boutiques) ; tableau type liste clé / valeur (pour des statistiques et ce genre de chose).
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m36s
Build & Push Images / build (core) (push) Successful in 1m53s
Build & Push Images / build-switcher (push) Successful in 25s
Build & Push Images / build (web) (push) Successful in 1m59s

Ajout de la possibilité de lié un PNJ à une page de lore
Ajout d'un graphe de liaison entre lore / PNJs
Passage en v.0.12.3-beta
This commit is contained in:
2026-06-12 13:23:46 +02:00
parent 7f519588b6
commit 14fc1c28fe
51 changed files with 1647 additions and 88 deletions

View File

@@ -14,7 +14,7 @@
<groupId>com.loremind</groupId>
<artifactId>loremind-core</artifactId>
<version>0.12.2-beta</version>
<version>0.12.3-beta</version>
<name>LoreMind Core</name>
<description>Backend Core - Architecture Hexagonale</description>

View File

@@ -163,7 +163,7 @@ public class CampaignImportService {
isBlank(p.description())
? java.util.Map.of()
: java.util.Map.of("Description", p.description().trim()),
null, null, campaignId, null, null));
null, null, campaignId, null, null, null));
created++;
}
return created;

View File

@@ -1,9 +1,12 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Campaign;
import com.loremind.domain.campaigncontext.Npc;
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
import com.loremind.domain.campaigncontext.ports.NpcRepository;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -16,9 +19,11 @@ import java.util.Optional;
public class NpcService {
private final NpcRepository npcRepository;
private final CampaignRepository campaignRepository;
public NpcService(NpcRepository npcRepository) {
public NpcService(NpcRepository npcRepository, CampaignRepository campaignRepository) {
this.npcRepository = npcRepository;
this.campaignRepository = campaignRepository;
}
public record NpcData(
@@ -29,6 +34,7 @@ public class NpcService {
Map<String, List<String>> imageValues,
Map<String, Map<String, String>> keyValueValues,
String campaignId,
List<String> relatedPageIds,
String folder,
Integer order
) {}
@@ -45,6 +51,7 @@ public class NpcService {
.imageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>())
.keyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>())
.campaignId(data.campaignId())
.relatedPageIds(data.relatedPageIds() != null ? new ArrayList<>(data.relatedPageIds()) : new ArrayList<>())
.folder(normalizeFolder(data.folder()))
.order(order)
.build();
@@ -59,6 +66,21 @@ public class NpcService {
return npcRepository.findByCampaignId(campaignId);
}
/**
* PNJ de TOUTES les campagnes liées au Lore donné (via {@code campaign.loreId}).
* Sert au graphe du Lore : relier les PNJ aux pages qu'ils référencent.
* Volume faible (usage mono-utilisateur) → filtrage en mémoire assumé.
*/
public List<Npc> getNpcsByLoreId(String loreId) {
List<Npc> out = new ArrayList<>();
for (Campaign campaign : campaignRepository.findAll()) {
if (campaign.isLinkedToLore() && campaign.getLoreId().equals(loreId)) {
out.addAll(npcRepository.findByCampaignId(campaign.getId()));
}
}
return out;
}
public Npc updateNpc(String id, NpcData data) {
Npc existing = npcRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Npc non trouvé avec l'ID: " + id));
@@ -68,6 +90,7 @@ public class NpcService {
existing.setValues(data.values() != null ? new HashMap<>(data.values()) : new HashMap<>());
existing.setImageValues(data.imageValues() != null ? new HashMap<>(data.imageValues()) : new HashMap<>());
existing.setKeyValueValues(data.keyValueValues() != null ? new HashMap<>(data.keyValueValues()) : new HashMap<>());
existing.setRelatedPageIds(data.relatedPageIds() != null ? new ArrayList<>(data.relatedPageIds()) : new ArrayList<>());
existing.setFolder(normalizeFolder(data.folder()));
if (data.order() != null) {
existing.setOrder(data.order());

View File

@@ -76,6 +76,8 @@ public class PageService {
existing.setNodeId(changes.getNodeId());
existing.setValues(CollectionUtils.copyMap(changes.getValues()));
existing.setImageValues(CollectionUtils.copyMap(changes.getImageValues()));
existing.setKeyValueValues(CollectionUtils.copyMap(changes.getKeyValueValues()));
existing.setTableValues(CollectionUtils.copyMap(changes.getTableValues()));
existing.setNotes(changes.getNotes());
existing.setTags(CollectionUtils.copyList(changes.getTags()));
existing.setRelatedPageIds(CollectionUtils.copyList(changes.getRelatedPageIds()));

View File

@@ -4,6 +4,7 @@ import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -46,6 +47,13 @@ public class Npc {
/** Référence vers la Campaign parente (cross-aggregate via ID). */
private String campaignId;
/**
* IDs de Pages de Lore référencées par ce PNJ (sa ville, sa faction, sa
* région…). Référence faible cross-context, même principe que sur
* Arc/Chapter/Scene — alimente notamment le graphe du Lore.
*/
private List<String> relatedPageIds;
/** Dossier de classement (texte libre, ex. « Bard's Gate »). Nullable = non classé. */
private String folder;
@@ -69,4 +77,9 @@ public class Npc {
if (keyValueValues == null) keyValueValues = new HashMap<>();
return keyValueValues;
}
public List<String> getRelatedPageIds() {
if (relatedPageIds == null) relatedPageIds = new ArrayList<>();
return relatedPageIds;
}
}

View File

@@ -39,6 +39,20 @@ public class Page {
*/
private Map<String, List<String>> imageValues;
/**
* Valeurs des champs KEY_VALUE_LIST (tableau libelle → valeur, comme sur les
* fiches de personnage) : fieldName → (label → valeur). Les labels sont
* definis par le Template ; seules les valeurs vivent sur la page.
*/
private Map<String, Map<String, String>> keyValueValues;
/**
* Valeurs des champs TABLE (colonnes figees au template, lignes libres) :
* fieldName → liste ordonnee de lignes, chaque ligne = colonne → cellule.
* Usage type : inventaire de boutique, table d'objets.
*/
private Map<String, List<Map<String, String>>> tableValues;
/** Notes privées du MJ (non exportées vers FoundryVTT). */
private String notes;

View File

@@ -9,6 +9,11 @@ package com.loremind.domain.shared.template;
* - KEY_VALUE_LIST : liste de paires {label, value} avec labels figes au template
* (Map<String, Map<String, String>> : fieldName -> label -> value).
* Usage : stat blocks, listes de competences, traits.
* - TABLE : tableau a colonnes figees au template (TemplateField.labels =
* noms de colonnes) et lignes LIBRES ajoutees au remplissage
* (Map<String, List<Map<String, String>>> : fieldName -> lignes,
* chaque ligne = colonne -> cellule).
* Usage : inventaire de boutique, tables d'objets, listes de prix.
* <p>
* Extension future possible : RICH_TEXT, DATE, BOOLEAN, REFERENCE...
*/
@@ -16,5 +21,6 @@ public enum FieldType {
TEXT,
IMAGE,
NUMBER,
KEY_VALUE_LIST
KEY_VALUE_LIST,
TABLE
}

View File

@@ -30,8 +30,9 @@ public class TemplateField {
/** Variante de rendu pour les champs IMAGE. Null = GALLERY. */
private ImageLayout layout;
/**
* Labels predefinis pour les champs KEY_VALUE_LIST (ordre significatif).
* Ex: ["FOR","DEX","CON","INT","SAG","CHA"] pour un champ "Caracteristiques".
* Labels predefinis (ordre significatif), selon le type :
* - KEY_VALUE_LIST : libelles des lignes. Ex: ["FOR","DEX","CON","INT","SAG","CHA"].
* - TABLE : noms des COLONNES. Ex: ["Objet","Prix","Description"].
* Null/vide pour les autres types.
*/
private List<String> labels;
@@ -70,4 +71,9 @@ public class TemplateField {
public static TemplateField keyValueList(String name, List<String> labels) {
return new TemplateField(name, FieldType.KEY_VALUE_LIST, null, labels);
}
/** Raccourci : construit un champ TABLE avec ses noms de colonnes. */
public static TemplateField table(String name, List<String> columns) {
return new TemplateField(name, FieldType.TABLE, null, columns);
}
}

View File

@@ -0,0 +1,51 @@
package com.loremind.infrastructure.persistence.converter;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Convertit une Map<String, List<Map<String, String>>> en JSON et inversement.
* <p>
* Utilise pour Page.tableValues : pour chaque champ TABLE du template, stocke
* la liste ordonnee des LIGNES du tableau, chaque ligne etant une map
* colonne -> cellule. Exemple :
* {"Inventaire": [{"Objet":"Potion","Prix":"50 po"}, {"Objet":"Corde","Prix":"1 po"}]}
* <p>
* Adaptateur technique pur : le domaine ignore ce converter.
*/
@Converter
public class StringRowListMapJsonConverter
implements AttributeConverter<Map<String, List<Map<String, String>>>, String> {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final TypeReference<Map<String, List<Map<String, String>>>> TYPE_REF =
new TypeReference<>() {};
@Override
public String convertToDatabaseColumn(Map<String, List<Map<String, String>>> attribute) {
if (attribute == null || attribute.isEmpty()) return "{}";
try {
return MAPPER.writeValueAsString(attribute);
} catch (Exception e) {
throw new IllegalStateException(
"Erreur serialisation Map<String, List<Map<String,String>>> -> JSON", e);
}
}
@Override
public Map<String, List<Map<String, String>>> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.isBlank()) return Collections.emptyMap();
try {
return MAPPER.readValue(dbData, TYPE_REF);
} catch (Exception e) {
throw new IllegalStateException(
"Erreur deserialisation JSON -> Map<String, List<Map<String,String>>>", e);
}
}
}

View File

@@ -86,7 +86,7 @@ public class TemplateFieldListJsonConverter
}
}
List<String> labels = null;
if (type == FieldType.KEY_VALUE_LIST) {
if (type == FieldType.KEY_VALUE_LIST || type == FieldType.TABLE) {
JsonNode labelsNode = item.path("labels");
if (labelsNode.isArray()) {
labels = new ArrayList<>();

View File

@@ -1,5 +1,6 @@
package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringListMapJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringMapMapJsonConverter;
@@ -54,6 +55,11 @@ public class NpcJpaEntity {
@Column(name = "campaign_id", nullable = false)
private Long campaignId;
/** IDs de Pages de Lore référencées (référence faible cross-context). JSON TEXT. */
@Convert(converter = StringListJsonConverter.class)
@Column(name = "related_page_ids", columnDefinition = "TEXT")
private List<String> relatedPageIds;
@Column(name = "folder")
private String folder;

View File

@@ -3,6 +3,8 @@ package com.loremind.infrastructure.persistence.entity;
import com.loremind.infrastructure.persistence.converter.StringListJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringListMapJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringMapJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringMapMapJsonConverter;
import com.loremind.infrastructure.persistence.converter.StringRowListMapJsonConverter;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -52,6 +54,16 @@ public class PageJpaEntity {
@Convert(converter = StringListMapJsonConverter.class)
private Map<String, List<String>> imageValues;
/** Valeurs des champs KEY_VALUE_LIST : fieldName → (label → valeur). JSON TEXT. */
@Column(name = "key_value_values", columnDefinition = "TEXT")
@Convert(converter = StringMapMapJsonConverter.class)
private Map<String, Map<String, String>> keyValueValues;
/** Valeurs des champs TABLE : fieldName → lignes (colonne → cellule). JSON TEXT. */
@Column(name = "table_values", columnDefinition = "TEXT")
@Convert(converter = StringRowListMapJsonConverter.class)
private Map<String, List<Map<String, String>>> tableValues;
@Column(columnDefinition = "TEXT")
private String notes;

View File

@@ -6,6 +6,7 @@ import com.loremind.infrastructure.persistence.entity.NpcJpaEntity;
import com.loremind.infrastructure.persistence.jpa.NpcJpaRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
@@ -59,6 +60,7 @@ public class PostgresNpcRepository implements NpcRepository {
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : new HashMap<>())
.keyValueValues(e.getKeyValueValues() != null ? new HashMap<>(e.getKeyValueValues()) : new HashMap<>())
.campaignId(e.getCampaignId().toString())
.relatedPageIds(e.getRelatedPageIds() != null ? new ArrayList<>(e.getRelatedPageIds()) : new ArrayList<>())
.folder(e.getFolder())
.order(e.getOrder())
.createdAt(e.getCreatedAt())
@@ -77,6 +79,7 @@ public class PostgresNpcRepository implements NpcRepository {
.imageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : new HashMap<>())
.keyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>())
.campaignId(Long.parseLong(n.getCampaignId()))
.relatedPageIds(n.getRelatedPageIds() != null ? new ArrayList<>(n.getRelatedPageIds()) : new ArrayList<>())
.folder(n.getFolder())
.order(n.getOrder())
.createdAt(n.getCreatedAt())

View File

@@ -93,6 +93,8 @@ public class PostgresPageRepository implements PageRepository {
.title(e.getTitle())
.values(e.getValues() != null ? new HashMap<>(e.getValues()) : new HashMap<>())
.imageValues(e.getImageValues() != null ? new HashMap<>(e.getImageValues()) : new HashMap<>())
.keyValueValues(e.getKeyValueValues() != null ? new HashMap<>(e.getKeyValueValues()) : new HashMap<>())
.tableValues(e.getTableValues() != null ? new HashMap<>(e.getTableValues()) : new HashMap<>())
.notes(e.getNotes())
.tags(e.getTags() != null ? new ArrayList<>(e.getTags()) : new ArrayList<>())
.relatedPageIds(e.getRelatedPageIds() != null ? new ArrayList<>(e.getRelatedPageIds()) : new ArrayList<>())
@@ -111,6 +113,8 @@ public class PostgresPageRepository implements PageRepository {
.title(p.getTitle())
.values(p.getValues() != null ? new HashMap<>(p.getValues()) : new HashMap<>())
.imageValues(p.getImageValues() != null ? new HashMap<>(p.getImageValues()) : new HashMap<>())
.keyValueValues(p.getKeyValueValues() != null ? new HashMap<>(p.getKeyValueValues()) : new HashMap<>())
.tableValues(p.getTableValues() != null ? new HashMap<>(p.getTableValues()) : new HashMap<>())
.notes(p.getNotes())
.tags(p.getTags() != null ? new ArrayList<>(p.getTags()) : new ArrayList<>())
.relatedPageIds(p.getRelatedPageIds() != null ? new ArrayList<>(p.getRelatedPageIds()) : new ArrayList<>())

View File

@@ -43,6 +43,15 @@ public class NpcController {
return ResponseEntity.ok(dtos);
}
/** PNJ de toutes les campagnes liées au Lore donné — alimente le graphe du Lore. */
@GetMapping("/lore/{loreId}")
public ResponseEntity<List<NpcDTO>> getNpcsByLore(@PathVariable String loreId) {
List<NpcDTO> dtos = npcService.getNpcsByLoreId(loreId).stream()
.map(npcMapper::toDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@PutMapping("/{id}")
public ResponseEntity<NpcDTO> updateNpc(@PathVariable String id, @RequestBody NpcDTO dto) {
Npc updated = npcService.updateNpc(id, toData(dto, dto.getOrder()));
@@ -64,6 +73,7 @@ public class NpcController {
dto.getImageValues(),
dto.getKeyValueValues(),
dto.getCampaignId(),
dto.getRelatedPageIds(),
dto.getFolder(),
order
);

View File

@@ -2,6 +2,7 @@ package com.loremind.infrastructure.web.dto.campaigncontext;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -20,6 +21,8 @@ public class NpcDTO {
private Map<String, List<String>> imageValues = new HashMap<>();
private Map<String, Map<String, String>> keyValueValues = new HashMap<>();
private String campaignId;
/** IDs de Pages de Lore référencées par ce PNJ (référence faible cross-context). */
private List<String> relatedPageIds = new ArrayList<>();
private String folder;
private int order;
}

View File

@@ -20,6 +20,10 @@ public class PageDTO {
private Map<String, String> values;
/** Pour chaque champ IMAGE du template, la liste ordonnee des IDs d'images. */
private Map<String, List<String>> imageValues;
/** Pour chaque champ KEY_VALUE_LIST du template : label → valeur. */
private Map<String, Map<String, String>> keyValueValues;
/** Pour chaque champ TABLE du template : lignes (colonne → cellule). */
private Map<String, List<Map<String, String>>> tableValues;
private String notes;
private List<String> tags;
private List<String> relatedPageIds;

View File

@@ -4,6 +4,7 @@ import com.loremind.domain.campaigncontext.Npc;
import com.loremind.infrastructure.web.dto.campaigncontext.NpcDTO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
@Component
@@ -20,6 +21,7 @@ public class NpcMapper {
dto.setImageValues(n.getImageValues() != null ? new HashMap<>(n.getImageValues()) : new HashMap<>());
dto.setKeyValueValues(n.getKeyValueValues() != null ? new HashMap<>(n.getKeyValueValues()) : new HashMap<>());
dto.setCampaignId(n.getCampaignId());
dto.setRelatedPageIds(n.getRelatedPageIds() != null ? new ArrayList<>(n.getRelatedPageIds()) : new ArrayList<>());
dto.setFolder(n.getFolder());
dto.setOrder(n.getOrder());
return dto;
@@ -36,6 +38,7 @@ public class NpcMapper {
.imageValues(dto.getImageValues() != null ? new HashMap<>(dto.getImageValues()) : new HashMap<>())
.keyValueValues(dto.getKeyValueValues() != null ? new HashMap<>(dto.getKeyValueValues()) : new HashMap<>())
.campaignId(dto.getCampaignId())
.relatedPageIds(dto.getRelatedPageIds() != null ? new ArrayList<>(dto.getRelatedPageIds()) : new ArrayList<>())
.folder(dto.getFolder())
.order(dto.getOrder())
.build();

View File

@@ -23,6 +23,8 @@ public class PageMapper {
dto.setTitle(page.getTitle());
dto.setValues(CollectionUtils.copyMap(page.getValues()));
dto.setImageValues(CollectionUtils.copyMap(page.getImageValues()));
dto.setKeyValueValues(CollectionUtils.copyMap(page.getKeyValueValues()));
dto.setTableValues(CollectionUtils.copyMap(page.getTableValues()));
dto.setNotes(page.getNotes());
dto.setTags(CollectionUtils.copyList(page.getTags()));
dto.setRelatedPageIds(CollectionUtils.copyList(page.getRelatedPageIds()));
@@ -41,6 +43,8 @@ public class PageMapper {
.title(dto.getTitle())
.values(CollectionUtils.copyMap(dto.getValues()))
.imageValues(CollectionUtils.copyMap(dto.getImageValues()))
.keyValueValues(CollectionUtils.copyMap(dto.getKeyValueValues()))
.tableValues(CollectionUtils.copyMap(dto.getTableValues()))
.notes(dto.getNotes())
.tags(CollectionUtils.copyList(dto.getTags()))
.relatedPageIds(CollectionUtils.copyList(dto.getRelatedPageIds()))

View File

@@ -29,7 +29,8 @@ public class TemplateFieldMapper {
layoutStr = layout.name();
}
List<String> labels = null;
if (field.getType() == FieldType.KEY_VALUE_LIST && field.getLabels() != null) {
if ((field.getType() == FieldType.KEY_VALUE_LIST || field.getType() == FieldType.TABLE)
&& field.getLabels() != null) {
labels = new ArrayList<>(field.getLabels());
}
return new TemplateFieldDTO(field.getName(), typeStr, layoutStr, labels);
@@ -54,7 +55,7 @@ public class TemplateFieldMapper {
}
}
List<String> labels = null;
if (type == FieldType.KEY_VALUE_LIST && dto.getLabels() != null) {
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);

View File

@@ -1,6 +1,7 @@
package com.loremind.application.campaigncontext;
import com.loremind.domain.campaigncontext.Npc;
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
import com.loremind.domain.campaigncontext.ports.NpcRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -29,6 +30,9 @@ public class NpcServiceTest {
@Mock
private NpcRepository npcRepository;
@Mock
private CampaignRepository campaignRepository;
@InjectMocks
private NpcService npcService;
@@ -51,7 +55,7 @@ public class NpcServiceTest {
Npc result = npcService.createNpc(
new NpcService.NpcData("Borin le forgeron", null, null,
Map.of("Notes", "Borin"), null, null, "camp-1", null,5));
Map.of("Notes", "Borin"), null, null, "camp-1", null, null, 5));
assertNotNull(result);
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
@@ -67,7 +71,7 @@ public class NpcServiceTest {
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of(a, b));
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
npcService.createNpc(new NpcService.NpcData("Nouveau", null, null, null, null, null, "camp-1", null,null));
npcService.createNpc(new NpcService.NpcData("Nouveau", null, null, null, null, null, "camp-1", null, null, null));
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
verify(npcRepository).save(captor.capture());
@@ -79,7 +83,7 @@ public class NpcServiceTest {
when(npcRepository.findByCampaignId("camp-1")).thenReturn(List.of());
when(npcRepository.save(any(Npc.class))).thenReturn(testNpc);
npcService.createNpc(new NpcService.NpcData("Premier", null, null, null, null, null, "camp-1", null,null));
npcService.createNpc(new NpcService.NpcData("Premier", null, null, null, null, null, "camp-1", null, null, null));
ArgumentCaptor<Npc> captor = ArgumentCaptor.forClass(Npc.class);
verify(npcRepository).save(captor.capture());
@@ -124,7 +128,7 @@ public class NpcServiceTest {
Npc result = npcService.updateNpc("npc-1",
new NpcService.NpcData("Borin renommé", null, null,
Map.of("Notes", "v2"), null, null, "camp-1", null,7));
Map.of("Notes", "v2"), null, null, "camp-1", null, null, 7));
assertEquals("Borin renommé", result.getName());
assertEquals("v2", result.getValues().get("Notes"));
@@ -138,7 +142,7 @@ public class NpcServiceTest {
Npc result = npcService.updateNpc("npc-1",
new NpcService.NpcData("Borin", null, null,
Map.of("Notes", "txt"), null, null, "camp-1", null,null));
Map.of("Notes", "txt"), null, null, "camp-1", null, null, null));
// testNpc avait order=1 → préservé
assertEquals(1, result.getOrder());
@@ -150,7 +154,7 @@ public class NpcServiceTest {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> npcService.updateNpc("missing",
new NpcService.NpcData("x", null, null, null, null, null, "camp-1", null,null)));
new NpcService.NpcData("x", null, null, null, null, null, "camp-1", null, null, null)));
assertTrue(ex.getMessage().contains("missing"));
verify(npcRepository, never()).save(any());
}