Voxel Klippen System weiter optimiert, Monolog System implementiert, Compiler Warnungen behoben
This commit is contained in:
@@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface AudioReference {
|
||||
|
||||
}
|
||||
public record AudioReference(String key) {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user