Atmosphäre-Tools, EZ-Tree-Fixes, i18n, AnimSet, Baum-Export

- blight-lang: TextResolver + EN/DE Sprachpakete (TextReference i18n)
- AnimSet: Clips + ActionMap in .animset.json zusammengeführt
- EZ-Tree: Branch-Parameter-Fixes (length/radius/children/force nicht senden,
  twist Grad→Radiant, leaves.size ×5); Ordner-ComboBox mit Auto-Refresh
- Logging beim Baum-Export in allen drei Generatoren (EZ-Tree, Blight, Palme)
- Atmosphäre-Tools: Emitter, Licht, Wasser, Sound-/Musikbereiche, Spiel-Starten
- AnimPreviewState, RetargetingSystem, AnimationLibrary (Animations-Editor)
- Terrain-Transparenz-Fix, Schatten-Fix, ThirdPersonCamera-Fix
- DayNightState, WeatherState, CloudsNode, JmeConsole
- MapIO v6, neue blight-common Modell-Klassen (GameCharacter, NPC, Quests…)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 23:12:46 +02:00
parent 1e0789461f
commit 50f496c864
121 changed files with 13277 additions and 806 deletions

View File

@@ -0,0 +1,86 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Partikel-Emitter als tab-separierte Textdatei
* ({@code blight_emitters.bpe}) neben der Kartendatei.
*/
public final class EmitterIO {
private EmitterIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_emitters.bpe");
}
public static void save(List<PlacedEmitter> emitters) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# x\ty\tz\tactivationRadius\ttexturePath\timagesX\timagesY"
+ "\tstartR\tstartG\tstartB\tstartA"
+ "\tendR\tendG\tendB\tendA"
+ "\tstartSize\tendSize"
+ "\tvelX\tvelY\tvelZ\tvelVariation"
+ "\tgravX\tgravY\tgravZ"
+ "\tlowLife\thighLife\tmaxParticles\temitRate");
w.newLine();
for (PlacedEmitter e : emitters) {
w.write(String.format(Locale.ROOT,
"%.5f\t%.5f\t%.5f\t%.5f\t%s\t%d\t%d"
+ "\t%.4f\t%.4f\t%.4f\t%.4f"
+ "\t%.4f\t%.4f\t%.4f\t%.4f"
+ "\t%.4f\t%.4f"
+ "\t%.4f\t%.4f\t%.4f\t%.4f"
+ "\t%.4f\t%.4f\t%.4f"
+ "\t%.4f\t%.4f\t%d\t%.4f%n",
e.x(), e.y(), e.z(), e.activationRadius(),
e.texturePath(), e.imagesX(), e.imagesY(),
e.startR(), e.startG(), e.startB(), e.startA(),
e.endR(), e.endG(), e.endB(), e.endA(),
e.startSize(), e.endSize(),
e.velX(), e.velY(), e.velZ(), e.velocityVariation(),
e.gravX(), e.gravY(), e.gravZ(),
e.lowLife(), e.highLife(), e.maxParticles(), e.emitRate()));
}
}
}
public static List<PlacedEmitter> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedEmitter> 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 < 28) continue;
try {
list.add(new PlacedEmitter(
Float.parseFloat(f[0]), // x
Float.parseFloat(f[1]), // y
Float.parseFloat(f[2]), // z
Float.parseFloat(f[3]), // activationRadius
f[4], // texturePath
Integer.parseInt(f[5]), // imagesX
Integer.parseInt(f[6]), // imagesY
Float.parseFloat(f[7]), Float.parseFloat(f[8]),
Float.parseFloat(f[9]), Float.parseFloat(f[10]), // startRGBA
Float.parseFloat(f[11]), Float.parseFloat(f[12]),
Float.parseFloat(f[13]), Float.parseFloat(f[14]), // endRGBA
Float.parseFloat(f[15]), Float.parseFloat(f[16]), // startSize, endSize
Float.parseFloat(f[17]), Float.parseFloat(f[18]),
Float.parseFloat(f[19]), Float.parseFloat(f[20]), // vel XYZ + var
Float.parseFloat(f[21]), Float.parseFloat(f[22]),
Float.parseFloat(f[23]), // grav XYZ
Float.parseFloat(f[24]), Float.parseFloat(f[25]), // lowLife, highLife
Integer.parseInt(f[26]), Float.parseFloat(f[27]) // maxParticles, emitRate
));
} catch (NumberFormatException ignored) {}
}
return list;
}
}

View File

@@ -0,0 +1,60 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Lichtquellen als tab-separierte Textdatei
* ({@code blight_lights.bll}) neben der Kartendatei.
*
* Spalten: x y z r g b intensity radius
*/
public final class LightIO {
private LightIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_lights.bll");
}
public static void save(List<PlacedLight> lights) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# x\ty\tz\tr\tg\tb\tintensity\tradius");
w.newLine();
for (PlacedLight l : lights) {
w.write(String.format(Locale.ROOT,
"%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
l.x(), l.y(), l.z(),
l.r(), l.g(), l.b(),
l.intensity(), l.radius()));
}
}
}
public static List<PlacedLight> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedLight> 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 x = Float.parseFloat(f[0]);
float y = Float.parseFloat(f[1]);
float z = Float.parseFloat(f[2]);
float r = Float.parseFloat(f[3]);
float g = Float.parseFloat(f[4]);
float b = Float.parseFloat(f[5]);
float intensity = Float.parseFloat(f[6]);
float radius = Float.parseFloat(f[7]);
list.add(new PlacedLight(x, y, z, r, g, b, intensity, radius));
} catch (NumberFormatException ignored) {}
}
return list;
}
}

View File

@@ -6,8 +6,8 @@ package de.blight.common;
* Basis-Terrain : 4097 × 4097 Vertices (= 4096 × 4096 Zellen),
* 8 Welteinheiten pro Zelle → Welt 2048 .. +2048.
* Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung.
* Splatmap : 513 × 513 Pixel (passt auf Spiel-Terrain 1:1).
* Kanäle R/G/B = Gewicht für Tex2/Tex3/Tex4; Tex1 füllt den Rest.
* Splatmap : 513 × 513 Pixel (1:1 zu beiden Terrain-Grids).
* Kanäle R/G/B/A = Gewicht für Tex1-Helligkeit / Tex2 / Tex3 / Tex4.
*/
public final class MapData {
@@ -29,6 +29,9 @@ public final class MapData {
/** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */
public static final int SPLAT_SIZE = 513;
/** Anzahl konfigurierbarer Textur-Slots pro Terrain-Layer. */
public static final int TEXTURE_SLOTS = 4;
// ── Daten ─────────────────────────────────────────────────────────────────
/** Y-Höhe jedes Vertex im Basis-Terrain [TERRAIN_VERTS²]. */
@@ -40,29 +43,55 @@ public final class MapData {
/** Y der Höhlendecke [UPPER_VERTS²]. */
public final float[] upperBottom;
/** 1 = Loch (offen), 0 = massiv [UPPER_CELLS²]. */
public final byte[] upperHole;
/** Splatmap Rot-Kanal: Tex1-Helligkeit (Alpha.R), immer 255 [SPLAT_SIZE²]. */
/** Splatmap Rot-Kanal: Tex1-Helligkeit (Alpha.R), immer 255 [SPLAT_SIZE²]. */
public final byte[] splatR;
/** Splatmap Grün-Kanal: Tex2 (Fels) mix-Faktor (Alpha.G) [SPLAT_SIZE²], Bytes 0255. */
/** Splatmap Grün-Kanal: Tex2-Blend (Alpha.G) [SPLAT_SIZE²]. */
public final byte[] splatG;
/** Splatmap Blau-Kanal: Tex3 (Erde) mix-Faktor (Alpha.B) [SPLAT_SIZE²], Bytes 0255. */
/** Splatmap Blau-Kanal: Tex3-Blend (Alpha.B) [SPLAT_SIZE²]. */
public final byte[] splatB;
/** Splatmap Alpha-Kanal: Tex4-Blend (Alpha.A) [SPLAT_SIZE²]. */
public final byte[] splatA;
/** Texturpfade für Basis-Terrain (4 Slots, "" = Standard-Textur). */
public final String[] terrainTextures = new String[]{"", "", "", ""};
/** Splatmap Rot-Kanal Gebirge: Tex1-Helligkeit, immer 255 [SPLAT_SIZE²]. */
public final byte[] upperSplatR;
/** Splatmap Grün-Kanal Gebirge: Tex2-Blend [SPLAT_SIZE²]. */
public final byte[] upperSplatG;
/** Splatmap Blau-Kanal Gebirge: Tex3-Blend [SPLAT_SIZE²]. */
public final byte[] upperSplatB;
/** Splatmap Alpha-Kanal Gebirge: Tex4-Blend [SPLAT_SIZE²]. */
public final byte[] upperSplatA;
/** Texturpfade für Gebirge (4 Slots, "" = Standard-Textur). */
public final String[] upperTextures = new String[]{"", "", "", ""};
/** Gras-Dichte [SPLAT_SIZE²], Bytes 0255 (0=kein Gras, 255=max Dichte). */
public final byte[] grassDensity;
/** Loch-Maske der oberen Schicht [UPPER_CELLS²], != 0 = Loch (Zelle ausgeblendet). */
public final byte[] upperHole;
/** Spawnpunkt X-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */
public float spawnX = 0f;
/** Spawnpunkt Z-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */
public float spawnZ = 0f;
public MapData() {
terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS];
upperTop = new float[UPPER_VERTS * UPPER_VERTS];
upperBottom = new float[UPPER_VERTS * UPPER_VERTS];
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
splatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
splatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
splatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
splatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperSplatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
}
}

View File

@@ -16,6 +16,9 @@ import java.util.zip.*;
* 1 Basis-Terrain + Obere Schicht (kein Splatmap)
* 2 wie 1 + Splatmap (R/G/B je 513×513 Bytes)
* 3 wie 2 + Gras-Dichte (513×513 Bytes)
* 4 wie 3 + Spawnpunkt (2× float)
* 5 wie 4 + Splatmap-Alpha + Texturpfade + Gebirge-Splatmap (RGBA + Pfade)
* 6 wie 5 ohne upperHole; upperTop/Bottom behalten (zukünftige Höhlen-Architektur)
*/
public final class MapIO {
@@ -47,7 +50,7 @@ public final class MapIO {
}
private static final int MAGIC = 0x424C4947; // "BLIG"
private static final int VERSION = 3;
private static final int VERSION = 6;
private MapIO() {}
@@ -84,13 +87,23 @@ public final class MapIO {
writeFloats(out, data.terrainHeight);
writeFloats(out, data.upperTop);
writeFloats(out, data.upperBottom);
out.write(data.upperHole);
// v2: splatmap
out.write(data.splatR);
out.write(data.splatG);
out.write(data.splatB);
// v3: gras-dichte
out.write(data.grassDensity);
// v4: spawnpunkt
out.writeFloat(data.spawnX);
out.writeFloat(data.spawnZ);
// v5: splatA + texturpfade + gebirge-splatmap
out.write(data.splatA);
writeStrings(out, data.terrainTextures);
out.write(data.upperSplatR);
out.write(data.upperSplatG);
out.write(data.upperSplatB);
out.write(data.upperSplatA);
writeStrings(out, data.upperTextures);
}
}
@@ -108,7 +121,10 @@ public final class MapIO {
readFloats(in, data.terrainHeight);
readFloats(in, data.upperTop);
readFloats(in, data.upperBottom);
in.readFully(data.upperHole);
if (version <= 5) {
// v5 had upperHole[UPPER_CELLS²]; read and discard
in.skip((long) MapData.UPPER_CELLS * MapData.UPPER_CELLS);
}
if (version >= 2) {
in.readFully(data.splatR);
@@ -118,7 +134,22 @@ public final class MapIO {
if (version >= 3) {
in.readFully(data.grassDensity);
}
// version 1/2: grassDensity stays all-zeros (= kein Gras)
if (version >= 4) {
data.spawnX = in.readFloat();
data.spawnZ = in.readFloat();
}
if (version >= 5) {
in.readFully(data.splatA);
readStrings(in, data.terrainTextures);
in.readFully(data.upperSplatR);
in.readFully(data.upperSplatG);
in.readFully(data.upperSplatB);
in.readFully(data.upperSplatA);
readStrings(in, data.upperTextures);
} else {
// Ältere Maps: upperSplatR auf 255 initialisieren (Tex1 immer sichtbar)
java.util.Arrays.fill(data.upperSplatR, (byte) 255);
}
}
return data;
}
@@ -137,4 +168,14 @@ public final class MapIO {
in.readFully(bytes);
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(arr);
}
private static void writeStrings(DataOutputStream out, String[] arr) throws IOException {
out.writeInt(arr.length);
for (String s : arr) out.writeUTF(s != null ? s : "");
}
private static void readStrings(DataInputStream in, String[] arr) throws IOException {
int len = in.readInt();
for (int i = 0; i < len && i < arr.length; i++) arr[i] = in.readUTF();
}
}

View File

@@ -0,0 +1,51 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
public final class MusicAreaIO {
private MusicAreaIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_music_areas.bma");
}
public static void save(List<PlacedMusicArea> areas) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# polygon\tdayTrack\tnightTrack\tcombatTrack");
w.newLine();
for (PlacedMusicArea a : areas) {
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
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<PlacedMusicArea> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedMusicArea> 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 {
float[][] pts = SoundAreaIO.decodePolygon(f[0]);
if (pts[0].length < 3) continue;
list.add(new PlacedMusicArea(pts[0], pts[1], f[1], f[2], f[3]));
} catch (Exception ignored) {}
}
return list;
}
}

View File

@@ -0,0 +1,52 @@
package de.blight.common;
public record PlacedEmitter(
float x, float y, float z,
float activationRadius,
String texturePath,
int imagesX, int imagesY,
float startR, float startG, float startB, float startA,
float endR, float endG, float endB, float endA,
float startSize, float endSize,
float velX, float velY, float velZ, float velocityVariation,
float gravX, float gravY, float gravZ,
float lowLife, float highLife,
int maxParticles, float emitRate
) {
public static PlacedEmitter fire(float x, float y, float z) {
return new PlacedEmitter(x, y + 0.5f, z, 20f,
"Effects/Explosion/flame.png", 2, 2,
1f, 0.7f, 0.1f, 1f,
1f, 0.1f, 0f, 0f,
0.5f, 1.2f,
0f, 2.5f, 0f, 0.5f,
0f, -0.1f, 0f,
1f, 3f,
60, 25f);
}
public static PlacedEmitter smoke(float x, float y, float z) {
return new PlacedEmitter(x, y + 1.5f, z, 20f,
"Effects/Smoke/Smoke.png", 1, 1,
0.5f, 0.5f, 0.5f, 0.4f,
0.2f, 0.2f, 0.2f, 0f,
0.8f, 2.5f,
0f, 1.2f, 0f, 0.3f,
0f, 0.05f, 0f,
3f, 6f,
24, 6f);
}
public static PlacedEmitter sparks(float x, float y, float z) {
return new PlacedEmitter(x, y + 0.3f, z, 20f,
"Effects/Explosion/spark.png", 1, 1,
1f, 0.9f, 0.2f, 1f,
1f, 0.3f, 0f, 0f,
0.05f, 0.02f,
0f, 4f, 0f, 1.0f,
0f, -5f, 0f,
0.5f, 2f,
50, 20f);
}
}

View File

@@ -0,0 +1,9 @@
package de.blight.common;
/** Unveränderliche Snapshot-Daten einer platzierten Lichtquelle auf der Karte. */
public record PlacedLight(
float x, float y, float z,
float r, float g, float b,
float intensity,
float radius
) {}

View File

@@ -0,0 +1,15 @@
package de.blight.common;
/** Unveränderliche Snapshot-Daten eines platzierten 3D-Modells auf der Karte. */
public record PlacedModel(
String modelPath, // "@box" / "@sphere" / ... / "models/foo.j3o"
float x, float y, float z,
float rotY, float rotX, float rotZ,
float scale,
boolean solid,
String texturePath, String normalMapPath, String materialPath,
/** Relativer Asset-Pfad zur exportierten j3o-Datei des Custom Meshes; "" wenn nicht verwendet. */
String meshFile,
/** AnimationLibrary-Clip-Key (z. B. "walk/Run"); "" = keine Animation. */
String animClip
) {}

View File

@@ -0,0 +1,77 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Modelle als tab-separierte Textdatei
* ({@code blight_objects.blo}) neben der Kartendatei.
*
* Spalten (seit v2):
* modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile
*
* Alte Dateien mit 6 Spalten (v1) werden gelesen; fehlende Felder erhalten Standardwerte.
*/
public final class PlacedModelIO {
private PlacedModelIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_objects.blo");
}
public static void save(List<PlacedModel> models) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip");
w.newLine();
for (PlacedModel m : models) {
w.write(String.format(Locale.ROOT,
"%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s%n",
m.modelPath(),
m.x(), m.y(), m.z(),
m.rotY(), m.scale(),
m.rotX(), m.rotZ(),
m.solid(),
nvl(m.texturePath()), nvl(m.normalMapPath()), nvl(m.materialPath()),
nvl(m.meshFile()), nvl(m.animClip())));
}
}
}
public static List<PlacedModel> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedModel> 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 < 6) continue;
try {
String modelPath = f[0];
float x = Float.parseFloat(f[1]);
float y = Float.parseFloat(f[2]);
float z = Float.parseFloat(f[3]);
float rotY = Float.parseFloat(f[4]);
float scale = Float.parseFloat(f[5]);
float rotX = f.length > 6 ? Float.parseFloat(f[6]) : 0f;
float rotZ = f.length > 7 ? Float.parseFloat(f[7]) : 0f;
boolean solid = f.length > 8 ? Boolean.parseBoolean(f[8]) : false;
String texPath = f.length > 9 ? f[9] : "";
String nmPath = f.length > 10 ? f[10] : "";
String matPath = f.length > 11 ? f[11] : "";
String meshFile = f.length > 12 ? f[12] : "";
String animClip = f.length > 13 ? f[13] : "";
list.add(new PlacedModel(modelPath, x, y, z,
rotY, rotX, rotZ, scale, solid,
texPath, nmPath, matPath, meshFile, animClip));
} catch (NumberFormatException ignored) {}
}
return list;
}
private static String nvl(String s) { return s != null ? s : ""; }
}

View File

@@ -0,0 +1,9 @@
package de.blight.common;
public record PlacedMusicArea(
float[] pointsX,
float[] pointsZ,
String dayTrack,
String nightTrack,
String combatTrack
) {}

View File

@@ -0,0 +1,9 @@
package de.blight.common;
public record PlacedSoundArea(
float[] pointsX,
float[] pointsZ,
String soundPath,
float volume,
boolean crossfade
) {}

View File

@@ -0,0 +1,7 @@
package de.blight.common;
/** Unveränderliche Daten einer platzierten Wasseroberfläche (Teich, See). */
public record PlacedWater(
float x, float y, float z,
float width, float depth
) {}

View File

@@ -0,0 +1,70 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
public final class SoundAreaIO {
private SoundAreaIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_sound_areas.bsa");
}
public static void save(List<PlacedSoundArea> areas) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# polygon\tsoundPath\tvolume\tcrossfade");
w.newLine();
for (PlacedSoundArea a : areas) {
w.write(encodePolygon(a.pointsX(), a.pointsZ()));
w.write('\t');
w.write(a.soundPath());
w.write('\t');
w.write(String.format(Locale.ROOT, "%.4f\t%b%n", a.volume(), a.crossfade()));
}
}
}
public static List<PlacedSoundArea> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedSoundArea> 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 {
float[][] pts = decodePolygon(f[0]);
if (pts[0].length < 3) continue;
list.add(new PlacedSoundArea(pts[0], pts[1], f[1],
Float.parseFloat(f[2]), Boolean.parseBoolean(f[3])));
} catch (Exception ignored) {}
}
return list;
}
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();
}
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,55 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Wasseroberflächen als tab-separierte Textdatei
* ({@code blight_water.blw}) neben der Kartendatei.
*
* Spalten: x y z width depth
*/
public final class WaterBodyIO {
private WaterBodyIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_water.blw");
}
public static void save(List<PlacedWater> bodies) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# x\ty\tz\twidth\tdepth");
w.newLine();
for (PlacedWater b : bodies) {
w.write(String.format(Locale.ROOT,
"%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
b.x(), b.y(), b.z(), b.width(), b.depth()));
}
}
}
public static List<PlacedWater> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<PlacedWater> 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 {
list.add(new PlacedWater(
Float.parseFloat(f[0]),
Float.parseFloat(f[1]),
Float.parseFloat(f[2]),
Float.parseFloat(f[3]),
Float.parseFloat(f[4])));
} catch (NumberFormatException ignored) {}
}
return list;
}
}

View File

@@ -0,0 +1,5 @@
package de.blight.common.model;
public interface AudioReference {
}

View File

@@ -0,0 +1,91 @@
package de.blight.common.model;
import com.google.gson.*;
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 GameCharacter}-Instanzen als JSON.
* Dateiformat: <id>.character im Verzeichnis character/
*
* JSON-Struktur:
* {
* "type": "MAIN_CHARACTER" | "NPC",
* "characterId": "hero",
* "name": "Der Held",
* "modelPath": "Models/hero.j3o",
* "animSetPath": "animations/sets/hero.j3o",
* ... (subclass fields via Gson)
* }
* Die Aktions-Zuweisung (IDLE → Clip-Name usw.) ist im AnimSet gespeichert
* (animSetPath.replaceAll(".j3o", "") + ".animset.json").
*/
public final class CharacterIO {
private static final Logger log = LoggerFactory.getLogger(CharacterIO.class);
private static final String EXTENSION = ".character";
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private CharacterIO() {}
// ── Save ──────────────────────────────────────────────────────────────────
public static void save(GameCharacter character, Path directory) throws IOException {
String id = character.getCharacterId();
if (id == null || id.isBlank()) throw new IllegalArgumentException("characterId must be set");
Files.createDirectories(directory);
JsonObject obj = (JsonObject) GSON.toJsonTree(character);
obj.addProperty("type", character instanceof MainCharacter ? "MAIN_CHARACTER" : "NPC");
Files.writeString(directory.resolve(id + EXTENSION),
GSON.toJson(obj), StandardCharsets.UTF_8);
log.debug("[CharacterIO] Gespeichert: {}", id);
}
// ── Load ──────────────────────────────────────────────────────────────────
public static GameCharacter load(Path file) throws IOException {
String json = Files.readString(file, StandardCharsets.UTF_8);
JsonObject obj = JsonParser.parseString(json).getAsJsonObject();
String type = obj.has("type") ? obj.get("type").getAsString() : "NPC";
GameCharacter c = "MAIN_CHARACTER".equals(type)
? GSON.fromJson(obj, MainCharacter.class)
: GSON.fromJson(obj, NPC.class);
log.debug("[CharacterIO] Geladen: {}", file.getFileName());
return c;
}
/** Lädt alle .character-Dateien aus dem angegebenen Verzeichnis. */
public static List<GameCharacter> loadAll(Path directory) {
if (!Files.isDirectory(directory)) return List.of();
List<GameCharacter> result = new ArrayList<>();
try (Stream<Path> walk = Files.list(directory)) {
walk.filter(p -> p.toString().endsWith(EXTENSION))
.sorted()
.forEach(p -> {
try { result.add(load(p)); } catch (IOException e) {
log.warn("[CharacterIO] Fehler beim Laden von {}: {}", p, e.getMessage());
}
});
} catch (IOException e) {
log.warn("[CharacterIO] Fehler beim Scannen von {}: {}", directory, e.getMessage());
}
return result;
}
/** Gibt true zurück wenn bereits eine MainCharacter-Datei im Verzeichnis existiert (außer der mit excludeId). */
public static boolean mainCharacterExists(Path directory, String excludeId) {
for (GameCharacter c : loadAll(directory)) {
if (c instanceof MainCharacter
&& !Objects.equals(c.getCharacterId(), excludeId)) return true;
}
return false;
}
}

View File

@@ -0,0 +1,12 @@
package de.blight.common.model;
import de.blight.common.model.quests.Quest;
public interface CharacterListener {
public void questFulfilled(Quest quest);
public void questAborted(Quest quest);
public void levelUp();
}

View File

@@ -0,0 +1,12 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Collectable implements Interactable {
private Item item;
private ObjectReference objectReference;
}

View File

@@ -0,0 +1,19 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
/**
* AlchemyTable
* EnchantmentTable
* Smithy
* Goldsmiths
* Workshop
*/
@Getter
@Setter
public class CraftingTable {
private TextReference name;
private ObjectReference object;
}

View File

@@ -0,0 +1,42 @@
package de.blight.common.model;
import java.util.ArrayList;
import java.util.List;
import de.blight.common.model.quests.Quest;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class DialogOption {
private int requiresChapter;
private Quest requiresQuestOpen;
private Quest 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 boolean enablesTrade;
public Status getRequiredStatus() {
if (requiresStatus == null) {
return Status.ENEMY;
}
return requiresStatus;
}
}

View File

@@ -0,0 +1,17 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public abstract class GameCharacter {
/** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */
private String characterId;
/** Anzeigename des Charakters — wird zur Laufzeit per TextReference aufgelöst. */
private TextReference name;
/** Relativer Pfad zum .j3o-Modell (relativ zu blight-assets/src/main/resources). */
private String modelPath;
/** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */
private String animSetPath;
}

View File

@@ -0,0 +1,5 @@
package de.blight.common.model;
public interface Interactable {
}

View File

@@ -0,0 +1,80 @@
package de.blight.common.model;
import java.util.HashMap;
import java.util.Map.Entry;
public class Inventar {
private HashMap<Item, Integer> items = new HashMap<Item, Integer>();
public void collect(Item item) {
add(item, 1);
}
public boolean remove(Item item, int count) {
if (items.containsKey(item) && items.get(item) >= count) {
items.merge(item, count * -1, Integer::sum);
if (items.get(item) <= 0) {
items.remove(item);
}
return true;
}
return false;
}
public boolean remove(Item item) {
return remove(item, 1);
}
public boolean remove(ItemCount itemCount) {
return remove(itemCount.getItem(), itemCount.getCount());
}
public boolean use(Item item, MainCharacter character) {
if (remove(item)) {
item.use(character);
return true;
}
return false;
}
public boolean hasItem(ItemCount itemCount) {
return hasItem(itemCount.getItem(), itemCount.getCount());
}
public boolean hasItem(Item item, Integer count) {
return items.containsKey(item) && items.get(item) >= count;
}
public void recieveItem(ItemCount itemCount) {
add(itemCount.getItem(), itemCount.getCount());
}
private void add(Item item) {
add(item, 1);
}
private void add(Item item, int count) {
if (items.containsKey(item)) {
items.merge(item, count, Integer::sum);
} else {
items.put(item, count);
}
}
public boolean canCraft(Recipe recipe) {
for (Entry<Item, Integer> entry : recipe.getComponents().entrySet()) {
if (!hasItem(entry.getKey(), entry.getValue())) {
return false;
}
}
return true;
}
public void craft(Recipe recipe) {
for (Entry<Item, Integer> entry : recipe.getComponents().entrySet()) {
remove(entry.getKey(), entry.getValue());
}
add(recipe.getCreates());
}
}

View File

@@ -0,0 +1,20 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Item {
private String itemId;
private ItemCategory category;
private TextReference name;
private TextReference description;
private int worthGold;
public void use(MainCharacter character) {
}
}

View File

@@ -0,0 +1,11 @@
package de.blight.common.model;
public enum ItemCategory {
WEAPON,
GEAR,
CONSUMABLES,
QUEST_ITEMS,
USABLES,
MISC;
}

View File

@@ -0,0 +1,7 @@
package de.blight.common.model;
public interface ItemCount {
public Item getItem();
public int getCount();
}

View File

@@ -0,0 +1,5 @@
package de.blight.common.model;
public interface Location {
}

View File

@@ -0,0 +1,78 @@
package de.blight.common.model;
import java.util.ArrayList;
import java.util.List;
import de.blight.common.model.quests.Quest;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MainCharacter extends GameCharacter {
private int chapter;
private Inventar inventar;
private int level;
private int xp;
private int currentHp;
private int maxHp;
private int currentStamina;
private int maxStamina;
private int currentMana;
private int myMana;
private List<Quest> openQuests;
private List<Quest> completedQuests;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private List<CharacterListener> listeners = new ArrayList<CharacterListener>();
public void handleDialogOption(DialogOption option) {
if (option.getRequiredItem() != null) {
getInventar().remove(option.getRequiredItem());
}
if (option.getRecievesItem() != null) {
getInventar().recieveItem(option.getRecievesItem());
}
if (option.getFulfillsQuest() != null ) {
fullfillQuest(option.getFulfillsQuest());
}
option.getAbortsQuests().forEach(this::abortQuest);
}
public void fullfillQuest(Quest quest) {
openQuests.remove(quest);
completedQuests.add(quest);
checkLevelUp(quest.getXp());
listeners.forEach(listener -> listener.questFulfilled(quest));
}
private void checkLevelUp(int questXp) {
var xpRequired = XPHelper.getXpRequired(level);
var newXp = xp + questXp;
if (newXp > xpRequired) {
this.level++;
this.xp = newXp - xpRequired;
listeners.forEach(CharacterListener::levelUp);
}
}
private void abortQuest(Quest quest) {
openQuests.remove(quest);
listeners.forEach(listener -> listener.questAborted(quest));
}
public void removeListener(CharacterListener listener) {
listeners.remove(listener);
}
public void addListener(CharacterListener listener) {
listeners.add(listener);
}
}

View File

@@ -0,0 +1,65 @@
package de.blight.common.model;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class NPC extends GameCharacter {
private static final Logger LOG = LoggerFactory.getLogger(NPC.class);
private Status status;
private boolean trader;
private List<Item> items;
private List<DialogOption> currentOptions;
public List<DialogOption> getAvailableOptions(MainCharacter character) {
return currentOptions.stream().filter(option ->
option.getRequiresChapter() < character.getChapter() &&
option.getRequiresStatus().requirementFulfilled(status) &&
(option.getRequiresQuestOpen() == null || character.getOpenQuests().contains(option.getRequiresQuestOpen())) &&
(option.getRequiredItem() == null || character.getInventar().hasItem(option.getRequiredItem())) &&
(option.getRequiresQuestComplete() == null || character.getCompletedQuests().contains(option.getRequiresQuestComplete()))).toList();
}
public boolean chooseDialogOption(DialogOption option, MainCharacter character) {
if (!currentOptions.contains(option)) {
LOG.warn("Dialog Option was choosen but is not available");
return false;
}
if (option.getRequiredItem() != null && !character.getInventar().hasItem(option.getRequiredItem())) {
LOG.warn("Dialog Option was choosen but required item is not in Inventar");
return false;
}
if (option.getRequiresQuestOpen() != null && !character.getOpenQuests().contains(option.getRequiresQuestOpen())) {
LOG.warn("Dialog Option was choosen but required quest is not open");
return false;
}
if (option.getRequiredStatus().requirementFulfilled(status)) {
LOG.warn("Dialog Option was choosen but required Status is not fulfilled");
return false;
}
if (option.getRequiresQuestComplete() != null && !character.getCompletedQuests().contains(option.getRequiresQuestComplete())) {
LOG.warn("Dialog Option was choosen but required Quest is not complete");
return false;
}
currentOptions.remove(option);
currentOptions.removeAll(option.getDisablesOptions());
currentOptions.addAll(option.getNextOptions());
character.handleDialogOption(option);
if (option.isEnablesTrade()) {
this.trader = true;
}
return true;
}
}

View File

@@ -0,0 +1,5 @@
package de.blight.common.model;
public interface ObjectReference {
}

View File

@@ -0,0 +1,12 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RecievesItem implements ItemCount {
private Item item;
private int count = 1;
}

View File

@@ -0,0 +1,15 @@
package de.blight.common.model;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Recipe {
private Item creates;
private Map<Item, Integer> components;
private Interactable requires;
}

View File

@@ -0,0 +1,13 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RequiredItem implements ItemCount {
private Item item;
private int count = 1;
private boolean gives;
}

View File

@@ -0,0 +1,16 @@
package de.blight.common.model;
public enum Status {
FRIENDLY(3), NEUTRAL(2), ENRAGED(1), ENEMY(0);
private int val;
private Status(int val) {
this.val = val;
}
public boolean requirementFulfilled(Status minVal) {
return this.val >= minVal.val;
}
}

View File

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

View File

@@ -0,0 +1,14 @@
package de.blight.common.model;
public class XPHelper {
private static final int BASE_VAL = 450;
private static final int GROTH_A = 5;
private static final int GROTH_B = 45;
private XPHelper() {}
public static int getXpRequired(int level) {
return (GROTH_A * (level * level)) + (GROTH_B * level) + BASE_VAL;
}
}

View File

@@ -0,0 +1,143 @@
package de.blight.common.model.abilities;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Abilities {
private int lvlMagic; // 1-10
private int lvlStaffCombat; // 1-10
private int lvlSwordsmanship; // 1-10
private int lvlArchery; // 1-10
private int lvlHeavyWeapons; // 1-10
private int lvlCrossbow; // 1-10
/**
* Level 1: Ermöglicht das Knacken von Schlössern
* Level 2: Ermöglicht den Taschendiebstahl
* Level 3: Auf leisen Sohlen - Schleichen ist effektiver
*/
private int lvlThievery; // 1-3
/**
* Level 1: Ermöglicht das Brauen von Trönken, die die Wirkung von Kräutern verstärken
* Level 2: Ermöglicht das Brauen von Tränken, die den Helden temporär
* Level 3: Ermöglicht das Brauen von Tränken, die den Helden dauerhaft verstärken
*/
private int lvlAlchemy; // 1-3
/**
* Level 1: Ermöglicht das Herstellen von Bandagen und das Herstellen von Spritzen, die temporär den Helden verbessern
* Level 2: Ermöglicht die Herstllung von Wurfbomben und ähnlichem
* Level 3: Ermöglicht die Verwendung von Attelerie
*/
private int lvlEngineering; // 1-3
public enum StaffAbilities {
BASE_ATTACK(1), // Eine Basisattacke mit einem Stab
BLOCK(3), // Ein Block für Nahkampfattacken
HEAVY_ATTACK(5), // Ein Schwerer Schlag, der aufgeladen werden kann
STEADINESS(7), // Erhöht für 3 Sekunden die Standfestigkeit und der Held nimmt weniger Schaden
FEINT(9), // Lässt den Gegner bei einem Angriff ins leere laufen
DISARM(10); // Entwaffnet einen Angreifer temporär
private int minLvl;
private StaffAbilities(int minLvl) {
this.minLvl = minLvl;
}
public int getMinLvl() {
return minLvl;
}
}
public enum MagicAbilities {
FIREBALL(1), // Feuert einen Feuerball auf einen Gegner der Schaden verursacht
LIGHT(1), // Erzeugt ein Licht, dass für 1 minute die umgebung erleuchtet
SHIELD(3), // Erzeugt für eine Sekunde einen Shild um den MainChar der Fernkampf Angriffe blockiert
ROOT(5), // Wurzelt alle Gegner im Umkreis von 20m am für 3 Sekunden Boden fest
SHOCKWAVE(7), // Feuert eine Shockwelle ab, die im Umkreis von 5 Metern die Gegner wegstößt, je dichter der Gegner, desto größer der Schaden
DRAIN(9), // Entzieht dem Gegner leben und heilt gleichzeitig den Helden
CHAIN_LIGHTNING(10); // Feuert einen Kettenblitz der auf mehrere Gegner überspringen und schaden verursachen kann
private int minLvl;
private MagicAbilities(int minLvl) {
this.minLvl = minLvl;
}
public int getMinLvl() {
return minLvl;
}
}
public enum SwordAbilities {
BASE_ATTACK(1), // Eine Basisattacke mit einem Schwert
BLOCK(3), // Ein Block für Nahkampfattacken
HEAVY_ATTACK(5), // Ein Schwerer Schlag, der aufgeladen werden kann
QUICK_THRUST(7), // Führt einen gezielten Stich durch, der einen Block teilweise ignoriert
BLADE_DANCE(9), // Führt mehrere schnelle Schläge nacheinander durch, kann einen Block durchbrechen
DEATH_BLOW(10); // Exekutiert Gegner mit weniger als 10% HP
private int minLvl;
private SwordAbilities(int minLvl) {
this.minLvl = minLvl;
}
public int getMinLvl() {
return minLvl;
}
}
public enum HeavyWeaponAbilities {
BASE_ATTACK(1), // Eine Basisattacke mit einem Zweihänder oder einer Helebarde
BLOCK(3), // Ein Block für Nahkampfattacken
HEAVY_ATTACK(5), // Ein Schwerer Schlag, der aufgeladen werden kann, Durchbricht sehr effektiv einen Block
SWIRL_ATTACK(7), // Rotiert mit der Waffe um sich und fügt allen Gegnern im Radius Schaden zu
DOUBLE_ATTACK(9), // Führt unmittelbar nacheinander zwei Agriff mit beiden enden der Waffe durch
EXECUTION(10); // Führt einen Angriff durch der kritischen Schaden verursacht
private int minLvl;
private HeavyWeaponAbilities(int minLvl) {
this.minLvl = minLvl;
}
public int getMinLvl() {
return minLvl;
}
}
public enum RangedWeaponAbilities {
BASE_ATTACK(1), // ermöglicht das Abfeuern einer Armbrust oder eines Bogens
PRECISION_SHOT(3), // Höhere Genauigkeit bei Schüssen auf Distanz
FAST_RELOAD(5), // Erhöht die Nachladegeschwindigkeit um 50%
LETHALITY(7), // Erhöht die Durchdringung von Schüssen mit Pfeilen oder Bolzen
DOUBLE_ATTACK(9), // ERmöglicht das Abfeuern von zwei Bolzen oder Pfeilen zur Selben Zeit, erhöht den Schaden
HAWK_EYE(10); // Zeitlupe für kurze Dauer beim Zielen, um Schwachstellen zu treffen.
private int minLvl;
private RangedWeaponAbilities(int minLvl) {
this.minLvl = minLvl;
}
public int getMinLvl() {
return minLvl;
}
}
public enum MiscAbilities {
SNEAK,
MINE_ORE,
GUT_ANIMALS,
ACROBATICS;
}
}

View File

@@ -0,0 +1,10 @@
package de.blight.common.model.abilities;
import de.blight.common.model.MainCharacter;
public interface Ability {
boolean requirementsFulfilled(MainCharacter character);
void used(MainCharacter character);
}

View File

@@ -0,0 +1,14 @@
package de.blight.common.model.quests;
import de.blight.common.model.Location;
import de.blight.common.model.NPC;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BringQuest {
private NPC bring;
private Location bringTo;
}

View File

@@ -0,0 +1,14 @@
package de.blight.common.model.quests;
import de.blight.common.model.Location;
import de.blight.common.model.NPC;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class FollowQuest implements QuestType {
private NPC follow;
private Location followTo;
}

View File

@@ -0,0 +1,12 @@
package de.blight.common.model.quests;
import de.blight.common.model.Interactable;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class InteractQuest implements QuestType {
private Interactable interactWith;
}

View File

@@ -0,0 +1,13 @@
package de.blight.common.model.quests;
import de.blight.common.model.Item;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ItemQuest implements QuestType {
private Item item;
private int count;
}

View File

@@ -0,0 +1,18 @@
package de.blight.common.model.quests;
import de.blight.common.model.TextReference;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Quest {
private int xp;
private String questId;
private TextReference text;
private TextReference description;
private TextReference successText;
private QuestType questType;
}

View File

@@ -0,0 +1,5 @@
package de.blight.common.model.quests;
public interface QuestType {
}

View File

@@ -0,0 +1,12 @@
package de.blight.common.model.quests;
import de.blight.common.model.NPC;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class TalkQuest implements QuestType {
private NPC talkTo;
}

View File

@@ -0,0 +1,57 @@
package de.blight.common.time;
import java.util.ArrayList;
import java.util.List;
/**
* Verwaltet die Spielzeit innerhalb eines Tages (0.01.0).
* 0.0 = Mitternacht, 0.25 = 6 Uhr (Sonnenaufgang), 0.5 = Mittag, 0.75 = 18 Uhr.
* Keine JME-Abhängigkeit reine Zeitlogik.
*/
public class DayTime {
/** Standard-Tagesdauer in Echtzeit-Sekunden (5 Minuten). */
public static final float DEFAULT_DAY_DURATION = 300f;
private float timeOfDay;
private float timeScale;
private boolean paused;
private final List<TimeListener> listeners = new ArrayList<>();
public DayTime() {
this(DEFAULT_DAY_DURATION);
}
/** @param dayDuration Echtzeit-Sekunden für einen kompletten Spieltag */
public DayTime(float dayDuration) {
this.timeScale = 1f / dayDuration;
this.timeOfDay = 0.25f; // Start: Sonnenaufgang
}
public void update(float tpf) {
if (paused) return;
timeOfDay = (timeOfDay + tpf * timeScale) % 1f;
for (TimeListener l : listeners) l.onTimeChanged(timeOfDay);
}
/** Tageszeit 0.01.0. */
public float getTimeOfDay() { return timeOfDay; }
public void setTimeOfDay(float t) { timeOfDay = ((t % 1f) + 1f) % 1f; }
/** @param seconds Echtzeit-Sekunden pro Spieltag */
public void setDayDuration(float seconds) { timeScale = 1f / seconds; }
public void setPaused(boolean paused) { this.paused = paused; }
public boolean isPaused() { return paused; }
/** Aktuelle Spielstunde (023). */
public int getHour() { return (int)(timeOfDay * 24f); }
/** Aktuelle Spielminute (059). */
public int getMinute() { return (int)(timeOfDay * 24f * 60f) % 60; }
public void addListener(TimeListener l) { listeners.add(l); }
public void removeListener(TimeListener l) { listeners.remove(l); }
}

View File

@@ -0,0 +1,7 @@
package de.blight.common.time;
@FunctionalInterface
public interface TimeListener {
/** @param timeOfDay 0.0 = Mitternacht, 0.25 = 6 Uhr, 0.5 = Mittag, 0.75 = 18 Uhr */
void onTimeChanged(float timeOfDay);
}