Commit vor großem Terrain refactoring
This commit is contained in:
53
blight-common/src/main/java/de/blight/common/AreaIO.java
Normal file
53
blight-common/src/main/java/de/blight/common/AreaIO.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public final class AreaIO {
|
||||
|
||||
private AreaIO() {}
|
||||
|
||||
public static Path getPath() {
|
||||
return MapIO.getMapPath().resolveSibling("blight_areas.bla");
|
||||
}
|
||||
|
||||
public static void save(List<PlacedArea> areas) throws IOException {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# polygon\tnameId\tdayTrack\tnightTrack\tcombatTrack");
|
||||
w.newLine();
|
||||
for (PlacedArea a : areas) {
|
||||
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
|
||||
w.write('\t');
|
||||
w.write(a.nameId());
|
||||
w.write('\t');
|
||||
w.write(a.dayTrack());
|
||||
w.write('\t');
|
||||
w.write(a.nightTrack());
|
||||
w.write('\t');
|
||||
w.write(a.combatTrack());
|
||||
w.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<PlacedArea> load() throws IOException {
|
||||
Path p = getPath();
|
||||
if (!Files.exists(p)) return List.of();
|
||||
List<PlacedArea> 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 < 5) continue;
|
||||
try {
|
||||
float[][] pts = SoundAreaIO.decodePolygon(f[0]);
|
||||
if (pts[0].length < 3) continue;
|
||||
list.add(new PlacedArea(pts[0], pts[1], f[1], f[2], f[3], f[4]));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package de.blight.common;
|
||||
|
||||
/** Einzeln platzierter Vertex-Gras-Halm. Y-Position wird beim Platzieren aus dem Terrain gebacken. */
|
||||
public record GrassVertexBlade(float x, float y, float z, float height, float dryness) {}
|
||||
@@ -0,0 +1,66 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.zip.*;
|
||||
|
||||
/**
|
||||
* Liest und schreibt Vertex-Gras-Halme als komprimierte Binärdatei
|
||||
* ({@code blight_grass_vertex.blgv}) neben der Kartendatei.
|
||||
*
|
||||
* Format v1: int MAGIC, int VERSION, int count, N × (float x, float y, float z, float height)
|
||||
* Format v2: wie v1 + float dryness pro Halm (0=grün, 0.5–1.0=gelb/braun)
|
||||
*/
|
||||
public final class GrassVertexIO {
|
||||
|
||||
private static final int MAGIC = 0x47565458; // "GVTX"
|
||||
private static final int VERSION = 2;
|
||||
|
||||
private GrassVertexIO() {}
|
||||
|
||||
public static Path getPath() {
|
||||
return MapIO.getMapPath().resolveSibling("blight_grass_vertex.blgv");
|
||||
}
|
||||
|
||||
public static void save(List<GrassVertexBlade> blades) throws IOException {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (DataOutputStream out = new DataOutputStream(
|
||||
new BufferedOutputStream(new GZIPOutputStream(Files.newOutputStream(p))))) {
|
||||
out.writeInt(MAGIC);
|
||||
out.writeInt(VERSION);
|
||||
out.writeInt(blades.size());
|
||||
for (GrassVertexBlade b : blades) {
|
||||
out.writeFloat(b.x());
|
||||
out.writeFloat(b.y());
|
||||
out.writeFloat(b.z());
|
||||
out.writeFloat(b.height());
|
||||
out.writeFloat(b.dryness());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<GrassVertexBlade> load() throws IOException {
|
||||
Path p = getPath();
|
||||
if (!Files.exists(p)) return List.of();
|
||||
try (DataInputStream in = new DataInputStream(
|
||||
new BufferedInputStream(new GZIPInputStream(Files.newInputStream(p))))) {
|
||||
int magic = in.readInt();
|
||||
if (magic != MAGIC) throw new IOException("Ungültiges Dateiformat (MAGIC)");
|
||||
int version = in.readInt();
|
||||
if (version < 1 || version > VERSION) throw new IOException("Unbekannte Version: " + version);
|
||||
int count = in.readInt();
|
||||
List<GrassVertexBlade> list = new ArrayList<>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
float x = in.readFloat();
|
||||
float y = in.readFloat();
|
||||
float z = in.readFloat();
|
||||
float h = in.readFloat();
|
||||
float dr = version >= 2 ? in.readFloat() : 0f;
|
||||
list.add(new GrassVertexBlade(x, y, z, h, dr));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
blight-common/src/main/java/de/blight/common/LocationIO.java
Normal file
70
blight-common/src/main/java/de/blight/common/LocationIO.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package de.blight.common;
|
||||
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
import de.blight.common.model.trigger.TriggerIO;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Liest und schreibt Locations ({@code blight_locations.blo}) neben der Kartendatei.
|
||||
*
|
||||
* Format (je Zeile): nameId TAB centerX TAB centerZ TAB radius TAB triggersJson
|
||||
*/
|
||||
public final class LocationIO {
|
||||
|
||||
private LocationIO() {}
|
||||
|
||||
public static Path getPath() {
|
||||
return MapIO.getMapPath().resolveSibling("blight_locations.blo");
|
||||
}
|
||||
|
||||
public static void save(List<Location> locations) throws IOException {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# nameId\tcenterX\tcenterZ\tradius\ttriggersJson");
|
||||
w.newLine();
|
||||
for (Location loc : locations) {
|
||||
w.write(loc.getId());
|
||||
w.write('\t');
|
||||
w.write(String.format(Locale.ROOT, "%.3f", loc.getCenterX()));
|
||||
w.write('\t');
|
||||
w.write(String.format(Locale.ROOT, "%.3f", loc.getCenterZ()));
|
||||
w.write('\t');
|
||||
w.write(String.format(Locale.ROOT, "%.3f", loc.getRadius()));
|
||||
w.write('\t');
|
||||
w.write(TriggerIO.serializeList(loc.getTriggers()));
|
||||
w.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Location> load() throws IOException {
|
||||
Path p = getPath();
|
||||
if (!Files.exists(p)) return List.of();
|
||||
List<Location> 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 < 4) continue;
|
||||
try {
|
||||
Location loc = new Location();
|
||||
loc.setName(new TextReference(f[0].strip()));
|
||||
loc.setCenterX(Float.parseFloat(f[1].strip()));
|
||||
loc.setCenterZ(Float.parseFloat(f[2].strip()));
|
||||
loc.setRadius(Float.parseFloat(f[3].strip()));
|
||||
if (f.length > 4) {
|
||||
List<Trigger> triggers = TriggerIO.deserializeList(f[4].strip());
|
||||
loc.setTriggers(triggers);
|
||||
}
|
||||
list.add(loc);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.blight.common;
|
||||
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
import de.blight.common.model.trigger.TriggerIO;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public final class LocationZoneIO {
|
||||
|
||||
private LocationZoneIO() {}
|
||||
|
||||
public static Path getPath() {
|
||||
return MapIO.getMapPath().resolveSibling("blight_location_zones.blz");
|
||||
}
|
||||
|
||||
public static void save(List<PlacedLocationZone> zones) throws IOException {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# polygon\tnameId\ttriggersJson");
|
||||
w.newLine();
|
||||
for (PlacedLocationZone z : zones) {
|
||||
w.write(SoundAreaIO.encodePolygon(z.pointsX(), z.pointsZ()));
|
||||
w.write('\t');
|
||||
w.write(z.nameId());
|
||||
w.write('\t');
|
||||
w.write(TriggerIO.serializeList(z.triggers()));
|
||||
w.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<PlacedLocationZone> load() throws IOException {
|
||||
Path p = getPath();
|
||||
if (!Files.exists(p)) return List.of();
|
||||
List<PlacedLocationZone> 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 < 2) continue;
|
||||
try {
|
||||
float[][] pts = SoundAreaIO.decodePolygon(f[0]);
|
||||
if (pts[0].length < 3) continue;
|
||||
List<Trigger> triggers = f.length > 2 ? TriggerIO.deserializeList(f[2]) : new ArrayList<>();
|
||||
list.add(new PlacedLocationZone(pts[0], pts[1], f[1], triggers));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,10 @@ public final class MapIO {
|
||||
|
||||
public static void save(MapData data) throws IOException {
|
||||
Files.createDirectories(MAP_PATH.getParent());
|
||||
Path tmp = MAP_PATH.resolveSibling(MAP_PATH.getFileName() + ".tmp");
|
||||
try (DataOutputStream out = new DataOutputStream(
|
||||
new BufferedOutputStream(
|
||||
new GZIPOutputStream(Files.newOutputStream(MAP_PATH))))) {
|
||||
new GZIPOutputStream(Files.newOutputStream(tmp))))) {
|
||||
out.writeInt(MAGIC);
|
||||
out.writeInt(VERSION);
|
||||
writeFloats(out, data.terrainHeight);
|
||||
@@ -127,6 +128,14 @@ public final class MapIO {
|
||||
for (int i = 0; i < slotEnd; i++) out.writeUTF(slots[i] != null ? slots[i] : "");
|
||||
out.write(data.grassTextureMap);
|
||||
}
|
||||
// Atomares Umbenennen: erst wenn die Datei vollständig ist ersetzen wir die alte.
|
||||
// ATOMIC_MOVE schlägt auf manchen Systemen cross-device fehl → Fallback auf REPLACE_EXISTING.
|
||||
try {
|
||||
Files.move(tmp, MAP_PATH, java.nio.file.StandardCopyOption.REPLACE_EXISTING,
|
||||
java.nio.file.StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (java.nio.file.AtomicMoveNotSupportedException e) {
|
||||
Files.move(tmp, MAP_PATH, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
public static MapData load() throws IOException {
|
||||
@@ -243,17 +252,32 @@ public final class MapIO {
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static final int FLOAT_CHUNK = 16384; // floats per I/O chunk (64 KB)
|
||||
|
||||
private static void writeFloats(DataOutputStream out, float[] arr) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(arr.length * Float.BYTES)
|
||||
.order(ByteOrder.BIG_ENDIAN);
|
||||
buf.asFloatBuffer().put(arr);
|
||||
out.write(buf.array());
|
||||
byte[] bytes = new byte[FLOAT_CHUNK * Float.BYTES];
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
|
||||
int offset = 0;
|
||||
while (offset < arr.length) {
|
||||
int count = Math.min(FLOAT_CHUNK, arr.length - offset);
|
||||
bb.clear();
|
||||
for (int i = 0; i < count; i++) bb.putFloat(arr[offset + i]);
|
||||
out.write(bytes, 0, count * Float.BYTES);
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
|
||||
private static void readFloats(DataInputStream in, float[] arr) throws IOException {
|
||||
byte[] bytes = new byte[arr.length * Float.BYTES];
|
||||
in.readFully(bytes);
|
||||
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(arr);
|
||||
byte[] bytes = new byte[FLOAT_CHUNK * Float.BYTES];
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
|
||||
int offset = 0;
|
||||
while (offset < arr.length) {
|
||||
int count = Math.min(FLOAT_CHUNK, arr.length - offset);
|
||||
in.readFully(bytes, 0, count * Float.BYTES);
|
||||
bb.clear();
|
||||
bb.asFloatBuffer().get(arr, offset, count);
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeStrings(DataOutputStream out, String[] arr) throws IOException {
|
||||
|
||||
28
blight-common/src/main/java/de/blight/common/ModelMeta.java
Normal file
28
blight-common/src/main/java/de/blight/common/ModelMeta.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package de.blight.common;
|
||||
|
||||
/**
|
||||
* Metadaten einer Model-Asset-Datei (.j3o.meta neben dem .j3o).
|
||||
* Skalierung ist per-Achse; uniformScale=true → X/Y/Z werden gemeinsam angepasst.
|
||||
*/
|
||||
public record ModelMeta(
|
||||
String name,
|
||||
String category,
|
||||
String tags,
|
||||
float scaleX,
|
||||
float scaleY,
|
||||
float scaleZ,
|
||||
boolean uniformScale,
|
||||
float pivotOffsetY,
|
||||
float placementOffsetY,
|
||||
boolean solid,
|
||||
boolean castShadow,
|
||||
boolean receiveShadow,
|
||||
float randomScaleMin,
|
||||
float randomScaleMax
|
||||
) {
|
||||
public static ModelMeta defaults(String j3oFileName) {
|
||||
String name = j3oFileName.replaceFirst("\\.j3o$", "");
|
||||
return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f,
|
||||
false, true, true, 1f, 1f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.blight.common;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.Properties;
|
||||
|
||||
/** Liest und schreibt {@code .j3o.meta}-Dateien neben dem jeweiligen {@code .j3o}. */
|
||||
public final class ModelMetaIO {
|
||||
|
||||
private ModelMetaIO() {}
|
||||
|
||||
public static Path metaPath(Path j3oPath) {
|
||||
return j3oPath.resolveSibling(j3oPath.getFileName() + ".meta");
|
||||
}
|
||||
|
||||
public static void save(ModelMeta m, Path j3oPath) throws IOException {
|
||||
Properties p = new Properties();
|
||||
p.setProperty("name", m.name());
|
||||
p.setProperty("category", m.category());
|
||||
p.setProperty("tags", m.tags());
|
||||
p.setProperty("scaleX", String.valueOf(m.scaleX()));
|
||||
p.setProperty("scaleY", String.valueOf(m.scaleY()));
|
||||
p.setProperty("scaleZ", String.valueOf(m.scaleZ()));
|
||||
p.setProperty("uniformScale", String.valueOf(m.uniformScale()));
|
||||
p.setProperty("pivotOffsetY", String.valueOf(m.pivotOffsetY()));
|
||||
p.setProperty("placementOffsetY", String.valueOf(m.placementOffsetY()));
|
||||
p.setProperty("solid", String.valueOf(m.solid()));
|
||||
p.setProperty("castShadow", String.valueOf(m.castShadow()));
|
||||
p.setProperty("receiveShadow", String.valueOf(m.receiveShadow()));
|
||||
p.setProperty("randomScaleMin", String.valueOf(m.randomScaleMin()));
|
||||
p.setProperty("randomScaleMax", String.valueOf(m.randomScaleMax()));
|
||||
try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) {
|
||||
p.store(w, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static ModelMeta load(Path j3oPath) {
|
||||
Path mp = metaPath(j3oPath);
|
||||
Properties p = new Properties();
|
||||
if (Files.exists(mp)) {
|
||||
try (Reader r = Files.newBufferedReader(mp)) {
|
||||
p.load(r);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
String defName = j3oPath.getFileName().toString().replaceFirst("\\.j3o$", "");
|
||||
return new ModelMeta(
|
||||
p.getProperty("name", defName),
|
||||
p.getProperty("category", ""),
|
||||
p.getProperty("tags", ""),
|
||||
parseFloat(p, "scaleX", 1f),
|
||||
parseFloat(p, "scaleY", 1f),
|
||||
parseFloat(p, "scaleZ", 1f),
|
||||
Boolean.parseBoolean(p.getProperty("uniformScale", "true")),
|
||||
parseFloat(p, "pivotOffsetY", 0f),
|
||||
parseFloat(p, "placementOffsetY", 0f),
|
||||
Boolean.parseBoolean(p.getProperty("solid", "false")),
|
||||
Boolean.parseBoolean(p.getProperty("castShadow", "true")),
|
||||
Boolean.parseBoolean(p.getProperty("receiveShadow", "true")),
|
||||
parseFloat(p, "randomScaleMin", 1f),
|
||||
parseFloat(p, "randomScaleMax", 1f)
|
||||
);
|
||||
}
|
||||
|
||||
private static float parseFloat(Properties p, String key, float def) {
|
||||
try { return Float.parseFloat(p.getProperty(key, String.valueOf(def))); }
|
||||
catch (NumberFormatException e) { return def; }
|
||||
}
|
||||
}
|
||||
10
blight-common/src/main/java/de/blight/common/PlacedArea.java
Normal file
10
blight-common/src/main/java/de/blight/common/PlacedArea.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package de.blight.common;
|
||||
|
||||
public record PlacedArea(
|
||||
float[] pointsX,
|
||||
float[] pointsZ,
|
||||
String nameId,
|
||||
String dayTrack,
|
||||
String nightTrack,
|
||||
String combatTrack
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.blight.common;
|
||||
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PlacedLocationZone(
|
||||
float[] pointsX,
|
||||
float[] pointsZ,
|
||||
String nameId,
|
||||
List<Trigger> triggers
|
||||
) {
|
||||
/** Rückwärtskompatibel: kein Trigger. */
|
||||
public PlacedLocationZone(float[] pointsX, float[] pointsZ, String nameId) {
|
||||
this(pointsX, pointsZ, nameId, List.of());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package de.blight.common;
|
||||
|
||||
/**
|
||||
* Platzierte Wasserfläche.
|
||||
* Die Form wird zur Laufzeit per Flood-Fill aus dem Geländenetz berechnet –
|
||||
* gespeichert werden nur Saatpunkt und Wasserstand.
|
||||
* Platzierte Wasserfläche, definiert durch ein Benutzer-Polygon, eine Höhe und eine Fließrichtung.
|
||||
*/
|
||||
public record PlacedWater(float seedX, float seedZ, float waterHeight) {}
|
||||
public record PlacedWater(float[] pointsX, float[] pointsZ, float waterHeight, float flowDegrees) {}
|
||||
|
||||
@@ -5,11 +5,9 @@ import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Liest und schreibt platzierte Wasserflächen als tab-separierte Textdatei
|
||||
* ({@code blight_water.blw}) neben der Kartendatei.
|
||||
* Liest und schreibt platzierte Wasserflächen ({@code blight_water.blw}) neben der Kartendatei.
|
||||
*
|
||||
* Format: seedX seedZ waterHeight
|
||||
* Die Form des Wasserkörpers wird per Flood-Fill zur Laufzeit rekonstruiert.
|
||||
* Format (je Zeile): x1,z1;x2,z2;x3,z3;... TAB waterHeight [TAB flowDegrees]
|
||||
*/
|
||||
public final class WaterBodyIO {
|
||||
|
||||
@@ -23,11 +21,20 @@ public final class WaterBodyIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# seedX\tseedZ\twaterHeight");
|
||||
w.write("# polygon_points\twaterHeight\tflowDegrees");
|
||||
w.newLine();
|
||||
for (PlacedWater b : bodies) {
|
||||
w.write(String.format(Locale.ROOT, "%.5f\t%.5f\t%.5f%n",
|
||||
b.seedX(), b.seedZ(), b.waterHeight()));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < b.pointsX().length; i++) {
|
||||
if (i > 0) sb.append(';');
|
||||
sb.append(String.format(Locale.ROOT, "%.5f,%.5f", b.pointsX()[i], b.pointsZ()[i]));
|
||||
}
|
||||
sb.append('\t');
|
||||
sb.append(String.format(Locale.ROOT, "%.5f", b.waterHeight()));
|
||||
sb.append('\t');
|
||||
sb.append(String.format(Locale.ROOT, "%.1f", b.flowDegrees()));
|
||||
w.write(sb.toString());
|
||||
w.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,14 +46,21 @@ public final class WaterBodyIO {
|
||||
for (String line : Files.readAllLines(p)) {
|
||||
line = line.strip();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
String[] f = line.split("\t", -1);
|
||||
if (f.length < 3) continue;
|
||||
String[] parts = line.split("\t", -1);
|
||||
if (parts.length < 2) continue;
|
||||
try {
|
||||
list.add(new PlacedWater(
|
||||
Float.parseFloat(f[0]),
|
||||
Float.parseFloat(f[1]),
|
||||
Float.parseFloat(f[2])));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
float wh = Float.parseFloat(parts[1].strip());
|
||||
float fd = parts.length >= 3 ? Float.parseFloat(parts[2].strip()) : 0f;
|
||||
String[] pts = parts[0].split(";", -1);
|
||||
float[] xs = new float[pts.length];
|
||||
float[] zs = new float[pts.length];
|
||||
for (int i = 0; i < pts.length; i++) {
|
||||
String[] c = pts[i].split(",", -1);
|
||||
xs[i] = Float.parseFloat(c[0].strip());
|
||||
zs[i] = Float.parseFloat(c[1].strip());
|
||||
}
|
||||
if (xs.length >= 3) list.add(new PlacedWater(xs, zs, wh, fd));
|
||||
} catch (NumberFormatException | ArrayIndexOutOfBoundsException ignored) {}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Collectable implements Interactable {
|
||||
|
||||
private Item item;
|
||||
private ObjectReference objectReference;
|
||||
}
|
||||
@@ -16,4 +16,14 @@ public class CraftingTable {
|
||||
|
||||
private TextReference name;
|
||||
private ObjectReference object;
|
||||
|
||||
private CraftingTableType type;
|
||||
|
||||
public enum CraftingTableType {
|
||||
AlchemyTable,
|
||||
EnchantmentTable,
|
||||
Smithy,
|
||||
Goldsmiths,
|
||||
Workshop;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Speichert genau eine {@link CraftingTable}-Instanz pro {@link CraftingTable.CraftingTableType}
|
||||
* als {@code <TypeName>.craftingtable}-JSON-Datei.
|
||||
*/
|
||||
public final class CraftingTableIO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CraftingTableIO.class);
|
||||
private static final String EXTENSION = ".craftingtable";
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private CraftingTableIO() {}
|
||||
|
||||
public static void save(CraftingTable table, Path dir) throws IOException {
|
||||
if (table.getType() == null) throw new IOException("CraftingTable ohne Typ kann nicht gespeichert werden.");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve(table.getType().name() + EXTENSION),
|
||||
GSON.toJson(toDto(table)), StandardCharsets.UTF_8);
|
||||
log.debug("[CraftingTableIO] Gespeichert: {}", table.getType().name());
|
||||
}
|
||||
|
||||
public static Optional<CraftingTable> load(CraftingTable.CraftingTableType type, Path dir) {
|
||||
Path file = dir.resolve(type.name() + EXTENSION);
|
||||
if (!Files.exists(file)) return Optional.empty();
|
||||
try {
|
||||
Dto dto = GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), Dto.class);
|
||||
return Optional.of(fromDto(dto, type));
|
||||
} catch (IOException e) {
|
||||
log.warn("[CraftingTableIO] Fehler beim Laden von {}: {}", type.name(), e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle konfigurierten Tische. Nicht vorhandene Typen fehlen in der Map.
|
||||
*/
|
||||
public static Map<CraftingTable.CraftingTableType, CraftingTable> loadAll(Path dir) {
|
||||
Map<CraftingTable.CraftingTableType, CraftingTable> result =
|
||||
new EnumMap<>(CraftingTable.CraftingTableType.class);
|
||||
for (CraftingTable.CraftingTableType type : CraftingTable.CraftingTableType.values())
|
||||
load(type, dir).ifPresent(t -> result.put(type, t));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void delete(CraftingTable.CraftingTableType type, Path dir) throws IOException {
|
||||
Files.deleteIfExists(dir.resolve(type.name() + EXTENSION));
|
||||
log.debug("[CraftingTableIO] Gelöscht: {}", type.name());
|
||||
}
|
||||
|
||||
// ── DTO ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private static Dto toDto(CraftingTable t) {
|
||||
Dto dto = new Dto();
|
||||
dto.type = t.getType() != null ? t.getType().name() : null;
|
||||
dto.nameId = t.getName() != null ? t.getName().id() : null;
|
||||
dto.objectPath = t.getObject() != null ? t.getObject().getPath() : null;
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static CraftingTable fromDto(Dto dto, CraftingTable.CraftingTableType fallbackType) {
|
||||
CraftingTable t = new CraftingTable();
|
||||
if (dto.type != null) {
|
||||
try { t.setType(CraftingTable.CraftingTableType.valueOf(dto.type)); }
|
||||
catch (IllegalArgumentException ignored) { t.setType(fallbackType); }
|
||||
} else {
|
||||
t.setType(fallbackType);
|
||||
}
|
||||
if (dto.nameId != null && !dto.nameId.isBlank())
|
||||
t.setName(new TextReference(dto.nameId));
|
||||
if (dto.objectPath != null && !dto.objectPath.isBlank())
|
||||
t.setObject(new ObjectReference(dto.objectPath));
|
||||
return t;
|
||||
}
|
||||
|
||||
private static class Dto {
|
||||
String type;
|
||||
String nameId;
|
||||
String objectPath;
|
||||
}
|
||||
}
|
||||
@@ -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.quests.Quest;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@@ -11,25 +11,28 @@ import lombok.Setter;
|
||||
@Setter
|
||||
public class DialogOption {
|
||||
|
||||
private String id = UUID.randomUUID().toString();
|
||||
private String label = "";
|
||||
|
||||
private int requiresChapter;
|
||||
private Quest requiresQuestOpen;
|
||||
private Quest requiresQuestComplete;
|
||||
private QuestRef requiresQuestOpen;
|
||||
private QuestRef requiresQuestComplete;
|
||||
private Status requiresStatus;
|
||||
|
||||
|
||||
private TextReference textHero;
|
||||
private AudioReference audioHero;
|
||||
private TextReference textNpc;
|
||||
private AudioReference audioNpc;
|
||||
|
||||
|
||||
private List<DialogOption> nextOptions;
|
||||
private List<DialogOption> disablesOptions;
|
||||
|
||||
|
||||
private RequiredItem requiredItem;
|
||||
private RecievesItem recievesItem;
|
||||
|
||||
private Quest recievesQuest;
|
||||
private Quest fulfillsQuest;
|
||||
private List<Quest> abortsQuests = new ArrayList<Quest>();
|
||||
|
||||
private QuestRef recievesQuest;
|
||||
private QuestRef fulfillsQuest;
|
||||
private List<QuestRef> abortsQuests = new ArrayList<>();
|
||||
|
||||
private boolean enablesTrade;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Fraction {
|
||||
|
||||
private UUID fractionId;
|
||||
private TextReference name;
|
||||
private TextReference maleMemberName;
|
||||
private TextReference femaleMemberName;
|
||||
private TextReference rank1Name;
|
||||
private TextReference rank2Name;
|
||||
private TextReference rank3Name;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
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 Fraction}-Instanzen als JSON.
|
||||
* Dateiformat: {@code <fractionId>.fraction} im fractions/-Verzeichnis.
|
||||
*/
|
||||
public final class FractionIO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FractionIO.class);
|
||||
private static final String EXTENSION = ".fraction";
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
public static final Comparator<Fraction> SORT_ORDER = Comparator
|
||||
.comparing((Fraction f) -> f.getName() != null ? f.getName().id() : "")
|
||||
.thenComparing(f -> f.getFractionId() != null ? f.getFractionId().toString() : "");
|
||||
|
||||
private FractionIO() {}
|
||||
|
||||
public static void save(Fraction fraction, Path dir) throws IOException {
|
||||
if (fraction.getFractionId() == null)
|
||||
throw new IOException("Fraction ohne ID kann nicht gespeichert werden.");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve(fraction.getFractionId() + EXTENSION),
|
||||
GSON.toJson(toDto(fraction)), StandardCharsets.UTF_8);
|
||||
log.debug("[FractionIO] Gespeichert: {}", fraction.getFractionId());
|
||||
}
|
||||
|
||||
public static Fraction load(Path file) throws IOException {
|
||||
Dto dto = GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), Dto.class);
|
||||
return fromDto(dto);
|
||||
}
|
||||
|
||||
public static List<Fraction> loadAll(Path dir) {
|
||||
List<Fraction> result = new ArrayList<>();
|
||||
if (!Files.isDirectory(dir)) return result;
|
||||
try (Stream<Path> walk = Files.list(dir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
.sorted()
|
||||
.forEach(p -> {
|
||||
try { result.add(load(p)); }
|
||||
catch (IOException e) { log.warn("[FractionIO] Fehler: {}", e.getMessage()); }
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("[FractionIO] Scan-Fehler: {}", e.getMessage());
|
||||
}
|
||||
result.sort(SORT_ORDER);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void delete(UUID fractionId, Path dir) throws IOException {
|
||||
if (fractionId != null)
|
||||
Files.deleteIfExists(dir.resolve(fractionId + EXTENSION));
|
||||
}
|
||||
|
||||
// ── DTO ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private static Dto toDto(Fraction f) {
|
||||
Dto dto = new Dto();
|
||||
dto.fractionId = f.getFractionId() != null ? f.getFractionId().toString() : null;
|
||||
dto.name = f.getName() != null ? f.getName().id() : null;
|
||||
dto.maleMemberName = f.getMaleMemberName() != null ? f.getMaleMemberName().id() : null;
|
||||
dto.femaleMemberName = f.getFemaleMemberName() != null ? f.getFemaleMemberName().id() : null;
|
||||
dto.rank1Name = f.getRank1Name() != null ? f.getRank1Name().id() : null;
|
||||
dto.rank2Name = f.getRank2Name() != null ? f.getRank2Name().id() : null;
|
||||
dto.rank3Name = f.getRank3Name() != null ? f.getRank3Name().id() : null;
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static Fraction fromDto(Dto dto) {
|
||||
Fraction f = new Fraction();
|
||||
if (dto.fractionId != null) {
|
||||
try { f.setFractionId(UUID.fromString(dto.fractionId)); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
f.setName(ref(dto.name));
|
||||
f.setMaleMemberName(ref(dto.maleMemberName));
|
||||
f.setFemaleMemberName(ref(dto.femaleMemberName));
|
||||
f.setRank1Name(ref(dto.rank1Name));
|
||||
f.setRank2Name(ref(dto.rank2Name));
|
||||
f.setRank3Name(ref(dto.rank3Name));
|
||||
return f;
|
||||
}
|
||||
|
||||
private static TextReference ref(String id) {
|
||||
return (id != null && !id.isBlank()) ? new TextReference(id) : null;
|
||||
}
|
||||
|
||||
private static class Dto {
|
||||
String fractionId;
|
||||
String name;
|
||||
String maleMemberName;
|
||||
String femaleMemberName;
|
||||
String rank1Name;
|
||||
String rank2Name;
|
||||
String rank3Name;
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ package de.blight.common.model;
|
||||
|
||||
public interface Interactable {
|
||||
|
||||
public String getDisplayText();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Platzhalter-Implementierung von Interactable mit einer ID für den Editor.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class InteractableRef implements Interactable {
|
||||
private String id;
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return id != null ? id : "";
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,21 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Item {
|
||||
public class Item implements Interactable {
|
||||
|
||||
private String itemId;
|
||||
private ItemCategory category;
|
||||
private TextReference name;
|
||||
private TextReference description;
|
||||
private int worthGold;
|
||||
|
||||
private ObjectReference modelRef;
|
||||
|
||||
public void use(MainCharacter character) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return TextRegistry.resolve(name, itemId != null ? itemId : "?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Lädt und speichert {@link Item}-Instanzen als JSON.
|
||||
* Dateiformat: {@code <itemId>.item} im items/-Verzeichnis.
|
||||
* Liste wird nach Kategorie-Ordinal, dann nach Name (TextReference-ID) sortiert.
|
||||
*/
|
||||
public final class ItemIO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ItemIO.class);
|
||||
private static final String EXTENSION = ".item";
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
public static final Comparator<Item> SORT_ORDER = Comparator
|
||||
.comparingInt((Item i) -> i.getCategory() != null ? i.getCategory().ordinal() : Integer.MAX_VALUE)
|
||||
.thenComparing(i -> i.getName() != null ? i.getName().id() : (i.getItemId() != null ? i.getItemId() : ""));
|
||||
|
||||
private ItemIO() {}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
public static void save(Item item, Path itemDir) throws IOException {
|
||||
if (item.getItemId() == null || item.getItemId().isBlank())
|
||||
throw new IllegalArgumentException("itemId darf nicht leer sein");
|
||||
Files.createDirectories(itemDir);
|
||||
Files.writeString(itemDir.resolve(item.getItemId() + EXTENSION),
|
||||
GSON.toJson(item), StandardCharsets.UTF_8);
|
||||
log.debug("[ItemIO] Gespeichert: {}", item.getItemId());
|
||||
}
|
||||
|
||||
public static Item load(Path file) throws IOException {
|
||||
return GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), Item.class);
|
||||
}
|
||||
|
||||
public static List<Item> loadAll(Path itemDir) {
|
||||
List<Item> result = new ArrayList<>();
|
||||
if (!Files.isDirectory(itemDir)) return result;
|
||||
try (Stream<Path> walk = Files.list(itemDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
.sorted()
|
||||
.forEach(p -> {
|
||||
try { result.add(load(p)); }
|
||||
catch (IOException e) { log.warn("[ItemIO] Fehler: {}", e.getMessage()); }
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemIO] Scan-Fehler: {}", e.getMessage());
|
||||
}
|
||||
result.sort(SORT_ORDER);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void delete(String itemId, Path itemDir) throws IOException {
|
||||
Files.deleteIfExists(itemDir.resolve(itemId + EXTENSION));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,33 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface Location {
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Location {
|
||||
|
||||
private TextReference name;
|
||||
private float centerX;
|
||||
private float centerZ;
|
||||
private float radius;
|
||||
private List<Trigger> triggers = new ArrayList<>();
|
||||
|
||||
/** Leitet die ID aus dem TextReference-Schlüssel ab – eindeutiger Bezeichner der Location. */
|
||||
public String getId() { return name != null ? name.id() : ""; }
|
||||
|
||||
public boolean contains(float x, float z) {
|
||||
float dx = x - centerX, dz = z - centerZ;
|
||||
return dx * dx + dz * dz <= radius * radius;
|
||||
}
|
||||
|
||||
public void entered(MainCharacter character) {
|
||||
triggers.stream()
|
||||
.filter(t -> t.isTriggarable(character))
|
||||
.forEach(t -> t.trigger(character));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ public class MainCharacter extends GameCharacter {
|
||||
|
||||
private List<Quest> openQuests;
|
||||
private List<Quest> completedQuests;
|
||||
private List<Quest> abortedQuests;
|
||||
|
||||
private de.blight.common.model.abilities.Abilities abilities;
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@@ -46,6 +49,10 @@ public class MainCharacter extends GameCharacter {
|
||||
option.getAbortsQuests().forEach(this::abortQuest);
|
||||
}
|
||||
|
||||
public void startQuest(Quest quest) {
|
||||
if (isQuestNew(quest)) openQuests.add(quest);
|
||||
}
|
||||
|
||||
public void fullfillQuest(Quest quest) {
|
||||
openQuests.remove(quest);
|
||||
completedQuests.add(quest);
|
||||
@@ -65,9 +72,14 @@ public class MainCharacter extends GameCharacter {
|
||||
|
||||
private void abortQuest(Quest quest) {
|
||||
openQuests.remove(quest);
|
||||
abortedQuests.add(quest);
|
||||
listeners.forEach(listener -> listener.questAborted(quest));
|
||||
}
|
||||
|
||||
public boolean isQuestNew(Quest quest) {
|
||||
return !openQuests.contains(quest) && !completedQuests.contains(quest) && !abortedQuests.contains(quest);
|
||||
}
|
||||
|
||||
public void removeListener(CharacterListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import lombok.Setter;
|
||||
public class NPC extends GameCharacter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NPC.class);
|
||||
|
||||
|
||||
private Status status;
|
||||
private boolean trader;
|
||||
private Fraction fraction;
|
||||
|
||||
private List<Item> items;
|
||||
|
||||
private List<DialogOption> currentOptions;
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface ObjectReference {
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Referenz auf ein 3D-Asset (z. B. ein .j3o-Modell).
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ObjectReference {
|
||||
|
||||
/** Relativer Asset-Pfad (z. B. {@code Models/Items/sword.j3o}). */
|
||||
private String path;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import de.blight.common.model.quests.Quest;
|
||||
|
||||
/**
|
||||
* Minimale Quest-Referenz (nur questId) für Verweise in DialogOption.
|
||||
* Kein vollständiger Quest-Datensatz.
|
||||
*/
|
||||
public class QuestRef extends Quest {
|
||||
// Nur die gemeinsamen Felder von Quest (questId, xp, texte) werden verwendet.
|
||||
// Typspezifische Felder sind nicht vorhanden.
|
||||
}
|
||||
@@ -11,5 +11,10 @@ public class Recipe {
|
||||
|
||||
private Item creates;
|
||||
private Map<Item, Integer> components;
|
||||
private Interactable requires;
|
||||
private CraftingTable table;
|
||||
|
||||
private Integer requiresLvlAlchemy;
|
||||
private Integer requiresLvlEngineering;
|
||||
private Integer requiresLvlSmithery;
|
||||
private Integer requiresLvlEnchanting;
|
||||
}
|
||||
|
||||
152
blight-common/src/main/java/de/blight/common/model/RecipeIO.java
Normal file
152
blight-common/src/main/java/de/blight/common/model/RecipeIO.java
Normal file
@@ -0,0 +1,152 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
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 Recipe}-Instanzen als JSON.
|
||||
* Dateiformat: {@code <createsItemId>.recipe} im recipes/-Verzeichnis.
|
||||
*
|
||||
* {@code Map<Item,Integer>} wird als Array von {@code {itemId, count}}-Einträgen
|
||||
* gespeichert, da Gson keine komplexen Map-Keys unterstützt.
|
||||
*/
|
||||
public final class RecipeIO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RecipeIO.class);
|
||||
private static final String EXTENSION = ".recipe";
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
public static final Comparator<Recipe> SORT_ORDER = Comparator
|
||||
.comparing((Recipe r) -> {
|
||||
if (r.getTable() == null) return "";
|
||||
CraftingTable.CraftingTableType t = r.getTable().getType();
|
||||
return t != null ? t.name() : "";
|
||||
})
|
||||
.thenComparing(r -> r.getCreates() != null ? safe(r.getCreates().getItemId()) : "");
|
||||
|
||||
private RecipeIO() {}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
public static void save(Recipe recipe, Path recipeDir) throws IOException {
|
||||
String fileId = fileId(recipe);
|
||||
Files.createDirectories(recipeDir);
|
||||
Files.writeString(recipeDir.resolve(fileId + EXTENSION),
|
||||
GSON.toJson(toDto(recipe)), StandardCharsets.UTF_8);
|
||||
log.debug("[RecipeIO] Gespeichert: {}", fileId);
|
||||
}
|
||||
|
||||
public static Recipe load(Path file) throws IOException {
|
||||
RecipeDto dto = GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), RecipeDto.class);
|
||||
return fromDto(dto);
|
||||
}
|
||||
|
||||
public static List<Recipe> loadAll(Path recipeDir) {
|
||||
List<Recipe> result = new ArrayList<>();
|
||||
if (!Files.isDirectory(recipeDir)) return result;
|
||||
try (Stream<Path> walk = Files.list(recipeDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
.sorted()
|
||||
.forEach(p -> {
|
||||
try { result.add(load(p)); }
|
||||
catch (IOException e) { log.warn("[RecipeIO] Fehler: {}", e.getMessage()); }
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("[RecipeIO] Scan-Fehler: {}", e.getMessage());
|
||||
}
|
||||
result.sort(SORT_ORDER);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Löscht die Datei des alten fileId (vor einer Umbenennung). */
|
||||
public static void delete(String oldCreatesItemId, Path recipeDir) throws IOException {
|
||||
if (oldCreatesItemId != null && !oldCreatesItemId.isBlank())
|
||||
Files.deleteIfExists(recipeDir.resolve(oldCreatesItemId + EXTENSION));
|
||||
}
|
||||
|
||||
public static String fileId(Recipe r) {
|
||||
if (r.getCreates() != null && r.getCreates().getItemId() != null
|
||||
&& !r.getCreates().getItemId().isBlank())
|
||||
return r.getCreates().getItemId();
|
||||
return "unbenanntes_rezept";
|
||||
}
|
||||
|
||||
// ── DTO conversion ────────────────────────────────────────────────────────
|
||||
|
||||
private static RecipeDto toDto(Recipe r) {
|
||||
RecipeDto dto = new RecipeDto();
|
||||
dto.createsItemId = r.getCreates() != null ? r.getCreates().getItemId() : null;
|
||||
if (r.getComponents() != null) {
|
||||
dto.components = new ArrayList<>();
|
||||
r.getComponents().forEach((item, count) ->
|
||||
dto.components.add(new ComponentDto(item.getItemId(), count)));
|
||||
dto.components.sort(Comparator.comparing(c -> safe(c.itemId)));
|
||||
}
|
||||
if (r.getTable() != null && r.getTable().getType() != null)
|
||||
dto.tableType = r.getTable().getType().name();
|
||||
dto.requiresLvlAlchemy = r.getRequiresLvlAlchemy();
|
||||
dto.requiresLvlEngineering = r.getRequiresLvlEngineering();
|
||||
dto.requiresLvlSmithery = r.getRequiresLvlSmithery();
|
||||
dto.requiresLvlEnchanting = r.getRequiresLvlEnchanting();
|
||||
return dto;
|
||||
}
|
||||
|
||||
static Recipe fromDto(RecipeDto dto) {
|
||||
Recipe r = new Recipe();
|
||||
if (dto.createsItemId != null) {
|
||||
Item creates = new Item();
|
||||
creates.setItemId(dto.createsItemId);
|
||||
r.setCreates(creates);
|
||||
}
|
||||
if (dto.components != null) {
|
||||
Map<Item, Integer> map = new LinkedHashMap<>();
|
||||
for (ComponentDto c : dto.components) {
|
||||
Item item = new Item();
|
||||
item.setItemId(c.itemId);
|
||||
map.put(item, c.count);
|
||||
}
|
||||
r.setComponents(map);
|
||||
}
|
||||
if (dto.tableType != null) {
|
||||
try {
|
||||
CraftingTable table = new CraftingTable();
|
||||
table.setType(CraftingTable.CraftingTableType.valueOf(dto.tableType));
|
||||
r.setTable(table);
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
r.setRequiresLvlAlchemy(dto.requiresLvlAlchemy);
|
||||
r.setRequiresLvlEngineering(dto.requiresLvlEngineering);
|
||||
r.setRequiresLvlSmithery(dto.requiresLvlSmithery);
|
||||
r.setRequiresLvlEnchanting(dto.requiresLvlEnchanting);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ── DTO classes ───────────────────────────────────────────────────────────
|
||||
|
||||
static class RecipeDto {
|
||||
String createsItemId;
|
||||
List<ComponentDto> components;
|
||||
String tableType;
|
||||
Integer requiresLvlAlchemy;
|
||||
Integer requiresLvlEngineering;
|
||||
Integer requiresLvlSmithery;
|
||||
Integer requiresLvlEnchanting;
|
||||
}
|
||||
|
||||
static class ComponentDto {
|
||||
String itemId;
|
||||
int count;
|
||||
ComponentDto(String itemId, int count) { this.itemId = itemId; this.count = count; }
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Ein Sprachpaket: Sprach-Code + Schlüssel→Text-Map. */
|
||||
@Getter
|
||||
@Setter
|
||||
public class TextBundle {
|
||||
|
||||
private String language;
|
||||
private Map<String, String> entries = new LinkedHashMap<>();
|
||||
|
||||
public TextBundle(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
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.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Lädt und speichert {@link TextBundle}-Instanzen als JSON.
|
||||
* Dateiformat: {@code <lang>.json} im localization/-Verzeichnis.
|
||||
*/
|
||||
public final class TextBundleIO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TextBundleIO.class);
|
||||
private static final String EXTENSION = ".json";
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Type MAP_TYPE = new TypeToken<LinkedHashMap<String, String>>(){}.getType();
|
||||
|
||||
private TextBundleIO() {}
|
||||
|
||||
public static void save(TextBundle bundle, Path dir) throws IOException {
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve(bundle.getLanguage() + EXTENSION),
|
||||
GSON.toJson(bundle.getEntries()), StandardCharsets.UTF_8);
|
||||
log.debug("[TextBundleIO] Gespeichert: {}", bundle.getLanguage());
|
||||
}
|
||||
|
||||
public static TextBundle load(Path file) throws IOException {
|
||||
String lang = file.getFileName().toString().replace(EXTENSION, "");
|
||||
Map<String, String> map = GSON.fromJson(
|
||||
Files.readString(file, StandardCharsets.UTF_8), MAP_TYPE);
|
||||
TextBundle bundle = new TextBundle(lang);
|
||||
if (map != null) bundle.setEntries(new LinkedHashMap<>(map));
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public static List<TextBundle> loadAll(Path dir) {
|
||||
List<TextBundle> result = new ArrayList<>();
|
||||
if (!Files.isDirectory(dir)) return result;
|
||||
try (Stream<Path> walk = Files.list(dir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
.sorted()
|
||||
.forEach(p -> {
|
||||
try { result.add(load(p)); }
|
||||
catch (IOException e) { log.warn("[TextBundleIO] Fehler: {}", e.getMessage()); }
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("[TextBundleIO] Scan-Fehler: {}", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<String> availableLanguages(Path dir) {
|
||||
List<String> langs = new ArrayList<>();
|
||||
if (!Files.isDirectory(dir)) return langs;
|
||||
try (Stream<Path> walk = Files.list(dir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
.map(p -> p.getFileName().toString().replace(EXTENSION, ""))
|
||||
.sorted()
|
||||
.forEach(langs::add);
|
||||
} catch (IOException ignored) {}
|
||||
return langs;
|
||||
}
|
||||
|
||||
public static void delete(String language, Path dir) throws IOException {
|
||||
Files.deleteIfExists(dir.resolve(language + EXTENSION));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Statische Auflösungstabelle für {@link TextReference}-Schlüssel zur Laufzeit und im Editor.
|
||||
* Wird durch {@code TextBundleIO} mit der aktiven Sprachversion befüllt.
|
||||
*/
|
||||
public final class TextRegistry {
|
||||
|
||||
private static final Map<String, String> entries = new HashMap<>();
|
||||
|
||||
private TextRegistry() {}
|
||||
|
||||
public static void registerAll(Map<String, String> map) {
|
||||
entries.putAll(map);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
/** Löst eine TextReference auf. Gibt den Schlüssel zurück wenn kein Eintrag vorhanden. */
|
||||
public static String resolve(TextReference ref) {
|
||||
if (ref == null || ref.id() == null) return "";
|
||||
return entries.getOrDefault(ref.id(), ref.id());
|
||||
}
|
||||
|
||||
public static String resolve(TextReference ref, String fallback) {
|
||||
if (ref == null) return fallback;
|
||||
return entries.getOrDefault(ref.id(), fallback);
|
||||
}
|
||||
|
||||
/** Direkter Zugriff für den Editor (alle Einträge). */
|
||||
public static Map<String, String> getAll() {
|
||||
return new HashMap<>(entries);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,16 @@ public class Abilities {
|
||||
*/
|
||||
private int lvlEngineering; // 1-3
|
||||
|
||||
/**
|
||||
* Levels ermöglichen das Schmieden von immer besseren Waffen und Ausrüstungen
|
||||
*/
|
||||
private int lvlSmithery; // 1-3
|
||||
|
||||
/**
|
||||
* Levels ermöglichen das HErstellen von mächtigeren Gegenständen
|
||||
*/
|
||||
private int lvlEnchanting; // 1-3
|
||||
|
||||
|
||||
public enum StaffAbilities {
|
||||
BASE_ATTACK(1), // Eine Basisattacke mit einem Stab
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BringQuest {
|
||||
public class BringQuest extends Quest {
|
||||
|
||||
private NPC bring;
|
||||
private Location bringTo;
|
||||
|
||||
@@ -7,7 +7,7 @@ import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public class FollowQuest implements QuestType {
|
||||
public class FollowQuest extends Quest {
|
||||
|
||||
private NPC follow;
|
||||
private Location followTo;
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class InteractQuest implements QuestType {
|
||||
public class InteractQuest extends Quest {
|
||||
|
||||
private Interactable interactWith;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class ItemQuest implements QuestType {
|
||||
public class ItemQuest extends Quest {
|
||||
|
||||
private Item item;
|
||||
private int count;
|
||||
|
||||
@@ -6,13 +6,11 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Quest {
|
||||
public abstract class Quest {
|
||||
|
||||
private int xp;
|
||||
private String questId;
|
||||
private TextReference text;
|
||||
private TextReference description;
|
||||
private TextReference successText;
|
||||
|
||||
private QuestType questType;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package de.blight.common.model.quests;
|
||||
|
||||
import com.google.gson.*;
|
||||
import de.blight.common.model.Interactable;
|
||||
import de.blight.common.model.InteractableRef;
|
||||
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;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Lädt und speichert {@link Quest}-Instanzen als JSON.
|
||||
* Dateiformat: {@code <questId>.quest} im quests/-Verzeichnis.
|
||||
*
|
||||
* Typ-Diskriminator im JSON: {@code "type": "BRING" | "FOLLOW" | "INTERACT" | "ITEM" | "TALK"}
|
||||
*/
|
||||
public final class QuestIO {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(QuestIO.class);
|
||||
private static final String EXTENSION = ".quest";
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.registerTypeAdapter(Quest.class, new QuestAdapter())
|
||||
.registerTypeAdapter(Interactable.class, new InteractableAdapter())
|
||||
.create();
|
||||
|
||||
private QuestIO() {}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
public static void save(Quest quest, Path questDir) throws IOException {
|
||||
if (quest.getQuestId() == null || quest.getQuestId().isBlank())
|
||||
throw new IllegalArgumentException("questId darf nicht leer sein");
|
||||
Files.createDirectories(questDir);
|
||||
JsonObject obj = serializeWithType(quest);
|
||||
Files.writeString(questDir.resolve(quest.getQuestId() + EXTENSION),
|
||||
GSON.toJson(obj), StandardCharsets.UTF_8);
|
||||
log.debug("[QuestIO] Gespeichert: {}", quest.getQuestId());
|
||||
}
|
||||
|
||||
public static Quest load(Path file) throws IOException {
|
||||
String json = Files.readString(file, StandardCharsets.UTF_8);
|
||||
return GSON.fromJson(json, Quest.class);
|
||||
}
|
||||
|
||||
public static List<Quest> loadAll(Path questDir) {
|
||||
List<Quest> result = new ArrayList<>();
|
||||
if (!Files.isDirectory(questDir)) return result;
|
||||
try (Stream<Path> walk = Files.list(questDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
.sorted()
|
||||
.forEach(p -> {
|
||||
try { result.add(load(p)); }
|
||||
catch (IOException e) { log.warn("[QuestIO] Fehler beim Laden: {}", e.getMessage()); }
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("[QuestIO] Verzeichnis-Scan fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void delete(String questId, Path questDir) throws IOException {
|
||||
Files.deleteIfExists(questDir.resolve(questId + EXTENSION));
|
||||
}
|
||||
|
||||
// ── Serialisation helper ──────────────────────────────────────────────────
|
||||
|
||||
public static String typeOf(Quest q) {
|
||||
if (q instanceof BringQuest) return "BRING";
|
||||
if (q instanceof FollowQuest) return "FOLLOW";
|
||||
if (q instanceof InteractQuest) return "INTERACT";
|
||||
if (q instanceof ItemQuest) return "ITEM";
|
||||
if (q instanceof TalkQuest) return "TALK";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
private static JsonObject serializeWithType(Quest quest) {
|
||||
// Serialize using the concrete type to capture all fields
|
||||
Gson plain = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.registerTypeAdapter(Interactable.class, new InteractableAdapter())
|
||||
.create();
|
||||
JsonObject obj = plain.toJsonTree(quest, quest.getClass()).getAsJsonObject();
|
||||
obj.addProperty("type", typeOf(quest));
|
||||
return obj;
|
||||
}
|
||||
|
||||
// ── Type adapters ─────────────────────────────────────────────────────────
|
||||
|
||||
static class QuestAdapter implements JsonDeserializer<Quest>, JsonSerializer<Quest> {
|
||||
|
||||
@Override
|
||||
public Quest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx)
|
||||
throws JsonParseException {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
String type = obj.has("type") ? obj.get("type").getAsString() : "UNKNOWN";
|
||||
// Deserialize as the concrete type (no type adapter registered for subtypes → no recursion)
|
||||
Gson plain = new GsonBuilder()
|
||||
.registerTypeAdapter(Interactable.class, new InteractableAdapter())
|
||||
.create();
|
||||
return switch (type) {
|
||||
case "BRING" -> plain.fromJson(json, BringQuest.class);
|
||||
case "FOLLOW" -> plain.fromJson(json, FollowQuest.class);
|
||||
case "INTERACT" -> plain.fromJson(json, InteractQuest.class);
|
||||
case "ITEM" -> plain.fromJson(json, ItemQuest.class);
|
||||
case "TALK" -> plain.fromJson(json, TalkQuest.class);
|
||||
default -> plain.fromJson(json, TalkQuest.class);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Quest src, Type typeOfSrc, JsonSerializationContext ctx) {
|
||||
return serializeWithType(src);
|
||||
}
|
||||
}
|
||||
|
||||
static class InteractableAdapter implements JsonDeserializer<Interactable>, JsonSerializer<Interactable> {
|
||||
|
||||
@Override
|
||||
public Interactable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx)
|
||||
throws JsonParseException {
|
||||
return new Gson().fromJson(json, InteractableRef.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Interactable src, Type typeOfSrc, JsonSerializationContext ctx) {
|
||||
return new Gson().toJsonTree(src, InteractableRef.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package de.blight.common.model.quests;
|
||||
|
||||
public interface QuestType {
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class TalkQuest implements QuestType {
|
||||
public class TalkQuest extends Quest {
|
||||
|
||||
private NPC talkTo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.blight.common.model.trigger;
|
||||
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.Status;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ändert den Status aller NPCs einer Fraktion (per Fraktions-UUID) wenn betreten.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class FractionStatusTrigger extends Trigger {
|
||||
|
||||
private UUID fractionId;
|
||||
private Status targetStatus;
|
||||
|
||||
@Override
|
||||
public boolean isTriggarableDelegate(MainCharacter character) {
|
||||
return fractionId != null && targetStatus != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(MainCharacter character) {
|
||||
// Laufzeit: alle NPCs der Fraktion suchen und Status setzen
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.blight.common.model.trigger;
|
||||
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.Status;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Ändert den Status eines bestimmten NPCs (per Character-ID) wenn betreten.
|
||||
* Die eigentliche NPC-Suche zur Laufzeit obliegt der Game-Registry.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class NpcStatusTrigger extends Trigger {
|
||||
|
||||
private String npcId;
|
||||
private Status targetStatus;
|
||||
|
||||
@Override
|
||||
public boolean isTriggarableDelegate(MainCharacter character) {
|
||||
return npcId != null && !npcId.isBlank() && targetStatus != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(MainCharacter character) {
|
||||
// Laufzeit: NPC per ID suchen und Status setzen
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.blight.common.model.trigger;
|
||||
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.quests.Quest;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class QuestStartTrigger extends Trigger {
|
||||
|
||||
private Quest quest;
|
||||
|
||||
@Override
|
||||
public boolean isTriggarableDelegate(MainCharacter character) {
|
||||
return quest != null && character.isQuestNew(quest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trigger(MainCharacter character) {
|
||||
if (quest != null) character.startQuest(quest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.blight.common.model.trigger;
|
||||
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public abstract class Trigger {
|
||||
|
||||
private int requiresChapter;
|
||||
|
||||
public boolean isTriggarable(MainCharacter character) {
|
||||
return character.getChapter() >= requiresChapter && isTriggarableDelegate(character);
|
||||
}
|
||||
|
||||
public abstract boolean isTriggarableDelegate(MainCharacter character);
|
||||
|
||||
public abstract void trigger(MainCharacter character);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package de.blight.common.model.trigger;
|
||||
|
||||
import com.google.gson.*;
|
||||
import de.blight.common.model.QuestRef;
|
||||
import de.blight.common.model.Status;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Gson-Serialisierung für {@link Trigger}-Instanzen mit Typ-Diskriminator.
|
||||
*
|
||||
* JSON-Format (kompakt, kein Pretty-Print für Inline-Verwendung in z. B. LocationZoneIO):
|
||||
* {@code [{"type":"QUEST_START","requiresChapter":0,"questId":"my_quest"}, ...]}
|
||||
*
|
||||
* Bekannte Typen:
|
||||
* <ul>
|
||||
* <li>{@code QUEST_START} → {@link QuestStartTrigger}</li>
|
||||
* <li>{@code NPC_STATUS} → {@link NpcStatusTrigger}</li>
|
||||
* <li>{@code FRACTION_STATUS} → {@link FractionStatusTrigger}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class TriggerIO {
|
||||
|
||||
public static final String TYPE_QUEST_START = "QUEST_START";
|
||||
public static final String TYPE_NPC_STATUS = "NPC_STATUS";
|
||||
public static final String TYPE_FRACTION_STATUS = "FRACTION_STATUS";
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter())
|
||||
.create();
|
||||
|
||||
private TriggerIO() {}
|
||||
|
||||
/** Serialisiert eine Trigger-Liste als kompaktes JSON (kein Zeilenumbruch). */
|
||||
public static String serializeList(List<Trigger> triggers) {
|
||||
if (triggers == null || triggers.isEmpty()) return "[]";
|
||||
return GSON.toJson(triggers);
|
||||
}
|
||||
|
||||
/** Deserialisiert eine Trigger-Liste aus JSON. Gibt leere Liste bei Fehler zurück. */
|
||||
public static List<Trigger> deserializeList(String json) {
|
||||
if (json == null || json.isBlank() || "[]".equals(json.strip())) return new ArrayList<>();
|
||||
try {
|
||||
JsonArray arr = JsonParser.parseString(json).getAsJsonArray();
|
||||
List<Trigger> result = new ArrayList<>();
|
||||
for (JsonElement el : arr) {
|
||||
Trigger t = GSON.fromJson(el, Trigger.class);
|
||||
if (t != null) result.add(t);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Adapter ───────────────────────────────────────────────────────────────
|
||||
|
||||
static class TriggerAdapter implements JsonSerializer<Trigger>, JsonDeserializer<Trigger> {
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Trigger src, Type typeOfSrc, JsonSerializationContext ctx) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("type", typeOf(src));
|
||||
obj.addProperty("requiresChapter", src.getRequiresChapter());
|
||||
|
||||
if (src instanceof QuestStartTrigger q) {
|
||||
if (q.getQuest() != null && q.getQuest().getQuestId() != null)
|
||||
obj.addProperty("questId", q.getQuest().getQuestId());
|
||||
} else if (src instanceof NpcStatusTrigger n) {
|
||||
if (n.getNpcId() != null) obj.addProperty("npcId", n.getNpcId());
|
||||
if (n.getTargetStatus() != null)
|
||||
obj.addProperty("targetStatus", n.getTargetStatus().name());
|
||||
} else if (src instanceof FractionStatusTrigger f) {
|
||||
if (f.getFractionId() != null)
|
||||
obj.addProperty("fractionId", f.getFractionId().toString());
|
||||
if (f.getTargetStatus() != null)
|
||||
obj.addProperty("targetStatus", f.getTargetStatus().name());
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Trigger deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext ctx)
|
||||
throws JsonParseException {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
String type = obj.has("type") ? obj.get("type").getAsString() : "";
|
||||
|
||||
Trigger t = switch (type) {
|
||||
case TYPE_QUEST_START -> {
|
||||
QuestStartTrigger q = new QuestStartTrigger();
|
||||
if (obj.has("questId")) {
|
||||
QuestRef ref = new QuestRef();
|
||||
ref.setQuestId(obj.get("questId").getAsString());
|
||||
q.setQuest(ref);
|
||||
}
|
||||
yield q;
|
||||
}
|
||||
case TYPE_NPC_STATUS -> {
|
||||
NpcStatusTrigger n = new NpcStatusTrigger();
|
||||
if (obj.has("npcId")) n.setNpcId(obj.get("npcId").getAsString());
|
||||
if (obj.has("targetStatus")) n.setTargetStatus(parseStatus(obj.get("targetStatus")));
|
||||
yield n;
|
||||
}
|
||||
case TYPE_FRACTION_STATUS -> {
|
||||
FractionStatusTrigger f = new FractionStatusTrigger();
|
||||
if (obj.has("fractionId")) {
|
||||
try { f.setFractionId(UUID.fromString(obj.get("fractionId").getAsString())); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
if (obj.has("targetStatus")) f.setTargetStatus(parseStatus(obj.get("targetStatus")));
|
||||
yield f;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (t != null && obj.has("requiresChapter"))
|
||||
t.setRequiresChapter(obj.get("requiresChapter").getAsInt());
|
||||
return t;
|
||||
}
|
||||
|
||||
private static String typeOf(Trigger t) {
|
||||
if (t instanceof QuestStartTrigger) return TYPE_QUEST_START;
|
||||
if (t instanceof NpcStatusTrigger) return TYPE_NPC_STATUS;
|
||||
if (t instanceof FractionStatusTrigger) return TYPE_FRACTION_STATUS;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
private static Status parseStatus(JsonElement el) {
|
||||
try { return Status.valueOf(el.getAsString()); }
|
||||
catch (IllegalArgumentException ignored) { return null; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user