Voxel Klippen System weiter optimiert, Monolog System implementiert, Compiler Warnungen behoben

This commit is contained in:
2026-07-23 20:29:51 +02:00
parent 9ebde59d40
commit 5a7d9897e4
65 changed files with 2365 additions and 179 deletions

View File

@@ -0,0 +1,50 @@
package de.blight.common;
import com.google.gson.*;
import de.blight.common.model.Monologue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Lädt und speichert Monolog-Definitionen als {@code blight_monologues.json}. */
public final class MonologueIO {
private static final Logger log = LoggerFactory.getLogger(MonologueIO.class);
private static final Gson GSON = de.blight.common.model.trigger.TriggerIO.registerAdapters(
new GsonBuilder().setPrettyPrinting()
).create();
private MonologueIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_monologues.json");
}
public static void save(List<Monologue> monologues) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
Files.writeString(p, GSON.toJson(monologues), StandardCharsets.UTF_8);
log.debug("[MonologueIO] {} Monolog(e) gespeichert.", monologues.size());
}
public static List<Monologue> load() {
Path p = getPath();
if (!Files.exists(p)) return new ArrayList<>();
try {
String json = Files.readString(p, StandardCharsets.UTF_8);
Type listType = new com.google.gson.reflect.TypeToken<List<Monologue>>(){}.getType();
List<Monologue> result = GSON.fromJson(json, listType);
return result != null ? result : new ArrayList<>();
} catch (Exception e) {
log.warn("[MonologueIO] Fehler beim Laden: {}", e.getMessage());
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,32 @@
package de.blight.common;
import de.blight.common.model.Monologue;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Laufzeit-Nachschlagetabelle für Monologe (ID → Monologue). Wird beim Spielstart befüllt. */
public final class MonologueRegistry {
private static final Map<String, Monologue> MAP = new HashMap<>();
private MonologueRegistry() {}
public static void init(List<Monologue> monologues) {
MAP.clear();
for (Monologue m : monologues) {
if (m.getId() != null && !m.getId().isBlank()) {
MAP.put(m.getId(), m);
}
}
}
public static Monologue get(String id) {
return id != null ? MAP.get(id) : null;
}
public static boolean isEmpty() {
return MAP.isEmpty();
}
}

View File

@@ -0,0 +1,13 @@
package de.blight.common;
public record PlacedVoxelCliffZone(
float[] pointsX,
float[] pointsZ,
float minOffset,
float maxHeight,
float noiseScale,
int octaves,
float persistence,
float edgeBlend,
int seed
) {}

View File

@@ -0,0 +1,77 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
public final class VoxelCliffZoneIO {
private VoxelCliffZoneIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_voxel_cliff_zones.bvcz");
}
public static void save(List<PlacedVoxelCliffZone> zones) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# polygon\tminOffset\tmaxHeight\tnoiseScale\toctaves\tpersistence\tedgeBlend\tseed");
w.newLine();
for (PlacedVoxelCliffZone z : zones) {
w.write(encodePolygon(z.pointsX(), z.pointsZ()));
w.write('\t');
w.write(String.format(Locale.ROOT, "%.4f\t%.4f\t%.6f\t%d\t%.4f\t%.4f\t%d%n",
z.minOffset(), z.maxHeight(), z.noiseScale(),
z.octaves(), z.persistence(), z.edgeBlend(), z.seed()));
}
}
}
public static List<PlacedVoxelCliffZone> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedVoxelCliffZone> list = new ArrayList<>();
for (String line : Files.readAllLines(p)) {
line = line.strip();
if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1);
if (f.length < 8) continue;
try {
float[][] pts = decodePolygon(f[0]);
if (pts[0].length < 3) continue;
list.add(new PlacedVoxelCliffZone(
pts[0], pts[1],
Float.parseFloat(f[1]),
Float.parseFloat(f[2]),
Float.parseFloat(f[3]),
Integer.parseInt(f[4]),
Float.parseFloat(f[5]),
Float.parseFloat(f[6]),
Integer.parseInt(f[7])));
} catch (Exception ignored) {}
}
return list;
}
private static String encodePolygon(float[] xs, float[] zs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < xs.length; i++) {
if (i > 0) sb.append(';');
sb.append(String.format(Locale.ROOT, "%.3f,%.3f", xs[i], zs[i]));
}
return sb.toString();
}
private static float[][] decodePolygon(String encoded) {
String[] pts = encoded.split(";", -1);
float[] xs = new float[pts.length];
float[] zs = new float[pts.length];
for (int i = 0; i < pts.length; i++) {
String[] xz = pts[i].split(",", -1);
xs[i] = Float.parseFloat(xz[0]);
zs[i] = Float.parseFloat(xz[1]);
}
return new float[][]{xs, zs};
}
}

View File

@@ -0,0 +1,20 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
import java.util.LinkedHashMap;
import java.util.Map;
/** Ein Audio-Paket: Sprach-Code + Schlüssel→Dateipfad-Map. */
@Getter
@Setter
public class AudioBundle {
private String language;
private Map<String, String> entries = new LinkedHashMap<>();
public AudioBundle(String language) {
this.language = language;
}
}

View File

@@ -0,0 +1,87 @@
package de.blight.common.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
/** Lädt und speichert {@link AudioBundle}-Instanzen als Properties-Dateien.
* Dateiformat: {@code audio_<lang>.properties} im lang/-Verzeichnis. */
public final class AudioBundleIO {
private static final Logger log = LoggerFactory.getLogger(AudioBundleIO.class);
private static final String PREFIX = "audio_";
private static final String EXTENSION = ".properties";
private AudioBundleIO() {}
public static void save(AudioBundle bundle, Path dir) throws IOException {
Files.createDirectories(dir);
Path file = dir.resolve(PREFIX + bundle.getLanguage() + EXTENSION);
try (BufferedWriter w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
for (Map.Entry<String, String> e : bundle.getEntries().entrySet()) {
w.write(escapeKey(e.getKey()) + "=" + escapeValue(e.getValue()));
w.newLine();
}
}
log.debug("[AudioBundleIO] Gespeichert: {}", file);
}
public static AudioBundle load(Path file) throws IOException {
String name = file.getFileName().toString();
String lang = name.replace(PREFIX, "").replace(EXTENSION, "");
Properties props = new Properties();
try (BufferedReader r = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
props.load(r);
}
LinkedHashMap<String, String> ordered = new LinkedHashMap<>();
try (BufferedReader r = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#") || line.startsWith("!")) continue;
int eq = line.indexOf('=');
int colon = line.indexOf(':');
int sep = (eq >= 0 && (colon < 0 || eq <= colon)) ? eq : colon;
if (sep < 0) continue;
String key = line.substring(0, sep).trim();
ordered.put(key, props.getProperty(key, ""));
}
}
AudioBundle bundle = new AudioBundle(lang);
bundle.setEntries(ordered);
return bundle;
}
public static AudioBundle loadOrEmpty(String lang, Path dir) {
Path file = dir.resolve(PREFIX + lang + EXTENSION);
if (!Files.exists(file)) {
return new AudioBundle(lang);
}
try {
return load(file);
} catch (IOException e) {
log.warn("[AudioBundleIO] Fehler beim Laden: {}", e.getMessage());
return new AudioBundle(lang);
}
}
public static void delete(String language, Path dir) throws IOException {
Files.deleteIfExists(dir.resolve(PREFIX + language + EXTENSION));
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static String escapeKey(String key) {
return key.replace(" ", "\\ ");
}
private static String escapeValue(String val) {
return val.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r");
}
}

View File

@@ -1,5 +1,3 @@
package de.blight.common.model;
public interface AudioReference {
}
public record AudioReference(String key) {}

View File

@@ -1,7 +1,11 @@
package de.blight.common.model;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import de.blight.common.model.quests.Quest;
import lombok.AccessLevel;
@@ -36,10 +40,18 @@ public class MainCharacter extends GameCharacter {
private List<Quest> abortedQuests;
private de.blight.common.model.abilities.Abilities abilities;
/** Gespielte Monolog-IDs wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */
private Set<String> playedMonologueIds = new HashSet<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private List<CharacterListener> listeners = new ArrayList<CharacterListener>();
/** Warteschlange für ausstehende Monologe wird NICHT serialisiert. */
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient Queue<Monologue> pendingMonologues = new ArrayDeque<>();
public void handleDialogOption(DialogOption option) {
if (option.getRequiredItem() != null) {
@@ -122,6 +134,25 @@ public class MainCharacter extends GameCharacter {
}
}
/** Stellt einen Monolog in die Warteschlange (wird vom JME-Render-Thread geleert). */
public void queueMonologue(Monologue m) {
if (pendingMonologues == null) pendingMonologues = new ArrayDeque<>();
pendingMonologues.offer(m);
}
/** Gibt den nächsten ausstehenden Monolog zurück oder null. */
public Monologue pollPendingMonologue() {
if (pendingMonologues == null) return null;
return pendingMonologues.poll();
}
/** Wendet die Quest-Folgen eines Monologs an. */
public void handleMonologue(Monologue m) {
if (m.getRecievesQuest() != null) startQuest(m.getRecievesQuest());
if (m.getFulfillsQuest() != null) fullfillQuest(m.getFulfillsQuest());
if (m.getAbortsQuests() != null) m.getAbortsQuests().forEach(this::abortQuest);
}
public void removeListener(CharacterListener listener) {
listeners.remove(listener);
}

View File

@@ -0,0 +1,22 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/** Selbstgespräch des Hauptcharakters: eine Sequenz von Held-Textschritten mit optionalen Quest-Folgen. */
@Getter
@Setter
public class Monologue {
private String id = "";
private List<DialogStep> heroSteps = new ArrayList<>();
private QuestRef recievesQuest;
private QuestRef fulfillsQuest;
private List<QuestRef> abortsQuests = new ArrayList<>();
private int requiresChapter;
/** Wenn true, wird der Monolog nach dem ersten Abspielen nicht erneut gefeuert. */
private boolean playOnce = true;
}

View File

@@ -0,0 +1,33 @@
package de.blight.common.model.trigger;
import de.blight.common.MonologueRegistry;
import de.blight.common.model.MainCharacter;
import de.blight.common.model.Monologue;
import lombok.Getter;
import lombok.Setter;
/** Startet einen Monolog des Hauptcharakters, wenn dieser eine Zone betritt. */
@Getter
@Setter
public class MonologueTrigger extends Trigger {
private String monologueId;
@Override
public boolean isTriggarableDelegate(MainCharacter character) {
Monologue m = MonologueRegistry.get(monologueId);
if (m == null) return false;
if (m.isPlayOnce() && character.getPlayedMonologueIds().contains(monologueId)) return false;
return true;
}
@Override
public void trigger(MainCharacter character) {
Monologue m = MonologueRegistry.get(monologueId);
if (m == null) return;
if (m.isPlayOnce()) {
character.getPlayedMonologueIds().add(monologueId);
}
character.queueMonologue(m);
}
}

View File

@@ -28,6 +28,7 @@ public final class TriggerIO {
public static final String TYPE_NPC_STATUS = "NPC_STATUS";
public static final String TYPE_FRACTION_STATUS = "FRACTION_STATUS";
public static final String TYPE_CHANGE_ROUTINE = "CHANGE_ROUTINE";
public static final String TYPE_MONOLOGUE = "MONOLOGUE";
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter())
@@ -87,6 +88,8 @@ public final class TriggerIO {
} else if (src instanceof ChangeRoutineTrigger r) {
if (r.getNpcId() != null) obj.addProperty("npcId", r.getNpcId());
if (r.getRoutineName() != null) obj.addProperty("routineName", r.getRoutineName());
} else if (src instanceof MonologueTrigger mo) {
if (mo.getMonologueId() != null) obj.addProperty("monologueId", mo.getMonologueId());
}
return obj;
}
@@ -128,6 +131,11 @@ public final class TriggerIO {
if (obj.has("routineName")) r.setRoutineName(obj.get("routineName").getAsString());
yield r;
}
case TYPE_MONOLOGUE -> {
MonologueTrigger mo = new MonologueTrigger();
if (obj.has("monologueId")) mo.setMonologueId(obj.get("monologueId").getAsString());
yield mo;
}
default -> null;
};
@@ -141,6 +149,7 @@ public final class TriggerIO {
if (t instanceof NpcStatusTrigger) return TYPE_NPC_STATUS;
if (t instanceof FractionStatusTrigger) return TYPE_FRACTION_STATUS;
if (t instanceof ChangeRoutineTrigger) return TYPE_CHANGE_ROUTINE;
if (t instanceof MonologueTrigger) return TYPE_MONOLOGUE;
return "UNKNOWN";
}