Character und Dialog System weiter gebaut

This commit is contained in:
2026-07-06 21:06:27 +02:00
parent b05eb1055d
commit 8bcee4491e
40 changed files with 2006 additions and 250 deletions

View File

@@ -30,5 +30,7 @@ public class SaveGame {
public Set<String> pickedUpItems = new HashSet<>();
/** IDs besiegter Gegner (für zukünftige Implementierung). */
public Set<String> defeatedEnemies = new HashSet<>();
/** NPC-ID → aktuell verfügbare Dialog-Option-IDs. */
public Map<String, List<String>> npcDialogState = new HashMap<>();
}
}

View File

@@ -31,8 +31,9 @@ public final class SaveGameIO {
if (sg.character == null) sg.character = new SaveGame.CharacterSave();
if (sg.character.inventory == null) sg.character.inventory = new java.util.LinkedHashMap<>();
if (sg.world == null) sg.world = new SaveGame.WorldSave();
if (sg.world.pickedUpItems == null) sg.world.pickedUpItems = new java.util.HashSet<>();
if (sg.world.pickedUpItems == null) sg.world.pickedUpItems = new java.util.HashSet<>();
if (sg.world.defeatedEnemies == null) sg.world.defeatedEnemies = new java.util.HashSet<>();
if (sg.world.npcDialogState == null) sg.world.npcDialogState = new java.util.HashMap<>();
return sg;
}

View File

@@ -32,7 +32,9 @@ public final class CharacterIO {
private static final Logger log = LoggerFactory.getLogger(CharacterIO.class);
private static final String EXTENSION = ".character";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Gson GSON = de.blight.common.model.trigger.TriggerIO.registerAdapters(
new GsonBuilder().setPrettyPrinting()
).create();
private CharacterIO() {}

View File

@@ -2,8 +2,8 @@ package de.blight.common.model;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import de.blight.common.model.trigger.Trigger;
import lombok.Getter;
import lombok.Setter;
@@ -11,8 +11,8 @@ import lombok.Setter;
@Setter
public class DialogOption {
private String id = UUID.randomUUID().toString();
private String label = "";
private String id = "";
private TextReference label;
private int requiresChapter;
private QuestRef requiresQuestOpen;
@@ -24,8 +24,8 @@ public class DialogOption {
private TextReference textNpc;
private AudioReference audioNpc;
private List<DialogOption> nextOptions;
private List<DialogOption> disablesOptions;
private transient List<DialogOption> nextOptions;
private transient List<DialogOption> disablesOptions;
private RequiredItem requiredItem;
private RecievesItem recievesItem;
@@ -33,8 +33,10 @@ public class DialogOption {
private QuestRef recievesQuest;
private QuestRef fulfillsQuest;
private List<QuestRef> abortsQuests = new ArrayList<>();
private boolean enablesTrade;
private List<Trigger> onChosenTriggers = new ArrayList<>();
public Status getRequiredStatus() {
if (requiresStatus == null) {

View File

@@ -48,7 +48,10 @@ public class MainCharacter extends GameCharacter {
if (option.getRecievesItem() != null) {
getInventar().recieveItem(option.getRecievesItem());
}
if (option.getFulfillsQuest() != null ) {
if (option.getRecievesQuest() != null) {
startQuest(option.getRecievesQuest());
}
if (option.getFulfillsQuest() != null) {
fullfillQuest(option.getFulfillsQuest());
}
option.getAbortsQuests().forEach(this::abortQuest);

View File

@@ -2,8 +2,11 @@ package de.blight.common.model;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -23,10 +26,24 @@ public class NPC extends GameCharacter implements Interactable {
private List<Item> items;
private List<DialogOption> currentOptions;
// ── Flache Dialog-Struktur (serialisiert) ─────────────────────────────────
/** Nur im Editor relevant: Optionen die nicht mit currentOptions verbunden sind, aber erhalten werden sollen. */
private List<DialogOption> editorOnlyOptions;
/** Alle Dialog-Optionen dieses NPCs, flach nach ID indiziert. */
private Map<String, DialogOption> dialogOptions = new LinkedHashMap<>();
/** IDs der Wurzel-Optionen (am Dialog-Start verfügbar). */
private List<String> dialogRoots = new ArrayList<>();
/** Baum: optionId → IDs der nachfolgenden Optionen. */
private Map<String, List<String>> dialogNext = new LinkedHashMap<>();
/** Baum: optionId → IDs der Optionen, die dadurch deaktiviert werden. */
private Map<String, List<String>> dialogDisables = new LinkedHashMap<>();
// ── Laufzeit-Zustand (transient, nicht serialisiert) ─────────────────────
/** Aktuell verfügbare Optionen wird per {@link #initRuntimeTree} aufgebaut. */
private transient List<DialogOption> currentOptions;
/**
* Standard-Nachrichten je Status, die angezeigt werden wenn keine Dialog-Optionen
@@ -98,12 +115,58 @@ public class NPC extends GameCharacter implements Interactable {
return null;
}
// ── Dialog-Initialisierung ────────────────────────────────────────────────
/**
* Baut den Laufzeit-Dialog-Zustand auf.
*
* @param activeOptionIds die aktuell aktiven Option-IDs (aus dem Spielstand),
* oder {@code null}/{@code empty} → {@link #dialogRoots} werden verwendet.
*/
public void initRuntimeTree(List<String> activeOptionIds) {
if (dialogOptions == null) dialogOptions = new LinkedHashMap<>();
// Transiente nextOptions / disablesOptions je DialogOption setzen
for (Map.Entry<String, List<String>> e : safe(dialogNext).entrySet()) {
DialogOption opt = dialogOptions.get(e.getKey());
if (opt == null) continue;
opt.setNextOptions(e.getValue().stream()
.map(dialogOptions::get).filter(Objects::nonNull)
.collect(Collectors.toList()));
}
for (Map.Entry<String, List<String>> e : safe(dialogDisables).entrySet()) {
DialogOption opt = dialogOptions.get(e.getKey());
if (opt == null) continue;
opt.setDisablesOptions(e.getValue().stream()
.map(dialogOptions::get).filter(Objects::nonNull)
.collect(Collectors.toList()));
}
// currentOptions aus Spielstand oder Wurzeln
List<String> ids = (activeOptionIds != null && !activeOptionIds.isEmpty())
? activeOptionIds : safe(dialogRoots);
this.currentOptions = ids.stream()
.map(dialogOptions::get).filter(Objects::nonNull)
.collect(Collectors.toList());
}
/** Gibt die aktuell aktiven Option-IDs zurück (für Spielstand-Persistenz). */
public List<String> currentOptionIds() {
if (currentOptions == null) return List.of();
return currentOptions.stream()
.map(DialogOption::getId).filter(Objects::nonNull)
.collect(Collectors.toList());
}
private static <K, V> Map<K, V> safe(Map<K, V> m) { return m != null ? m : Map.of(); }
private static <T> List<T> safe(List<T> l) { return l != null ? l : List.of(); }
// ── Dialog-Methoden ──────────────────────────────────────────────────────
public List<DialogOption> getAvailableOptions(MainCharacter character) {
return currentOptions.stream().filter(option ->
option.getRequiresChapter() < character.getChapter() &&
option.getRequiresStatus().requirementFulfilled(status) &&
option.getRequiresChapter() <= character.getChapter() &&
(option.getRequiresStatus() == null || status.requirementFulfilled(option.getRequiresStatus())) &&
(option.getRequiresQuestOpen() == null || character.getOpenQuests().contains(option.getRequiresQuestOpen())) &&
(option.getRequiredItem() == null || character.getInventar().hasItem(option.getRequiredItem())) &&
(option.getRequiresQuestComplete() == null || character.getCompletedQuests().contains(option.getRequiresQuestComplete()))).toList();

View File

@@ -0,0 +1,51 @@
package de.blight.common.model;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.SortedSet;
import java.util.TreeSet;
public final class TextKeyStore {
private static final LinkedHashSet<String> keys = new LinkedHashSet<>();
private static Path keysFile = null;
private TextKeyStore() {}
/** Muss vom Editor beim Start aufgerufen werden. Lädt bestehende Keys aus der Datei. */
public static void init(Path file) {
keysFile = file;
if (Files.exists(file)) {
try {
Files.lines(file, StandardCharsets.UTF_8)
.map(String::trim)
.filter(l -> !l.isBlank())
.forEach(keys::add);
} catch (IOException ignored) {}
}
}
/** Registriert einen Key. Im Spiel-Modus (keysFile == null) kein Schreiben. */
public static void register(String id) {
if (id == null || id.isBlank()) {
return;
}
if (keys.add(id) && keysFile != null) {
save();
}
}
/** Alphabetisch sortierte Kopie aller bekannten Keys. */
public static SortedSet<String> getKeys() {
return new TreeSet<>(keys);
}
private static void save() {
try {
Files.write(keysFile, new TreeSet<>(keys), StandardCharsets.UTF_8);
} catch (IOException ignored) {}
}
}

View File

@@ -1,3 +1,7 @@
package de.blight.common.model;
public record TextReference(String id) {}
public record TextReference(String id) {
public TextReference {
TextKeyStore.register(id);
}
}

View File

@@ -3,6 +3,8 @@ package de.blight.common.model;
/** Weltposition (x, y, z in Metern). y = 0 bedeutet "Terrain-Höhe verwenden". */
public class WorldPoint {
public float x, y, z;
/** Blickrichtung um Y-Achse in Radiant. 0 = Richtung +Z. */
public float yaw = 0f;
public WorldPoint() {}

View File

@@ -35,6 +35,11 @@ public final class TriggerIO {
private TriggerIO() {}
/** Registriert den Trigger-Typ-Adapter an einem GsonBuilder (für andere Module). */
public static GsonBuilder registerAdapters(GsonBuilder builder) {
return builder.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter());
}
/** Serialisiert eine Trigger-Liste als kompaktes JSON (kein Zeilenumbruch). */
public static String serializeList(List<Trigger> triggers) {
if (triggers == null || triggers.isEmpty()) return "[]";