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:
@@ -21,3 +21,10 @@ compileJava.options.encoding = 'UTF-8'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.projectlombok:lombok:1.18.38'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
}
|
||||
|
||||
86
blight-common/src/main/java/de/blight/common/EmitterIO.java
Normal file
86
blight-common/src/main/java/de/blight/common/EmitterIO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
60
blight-common/src/main/java/de/blight/common/LightIO.java
Normal file
60
blight-common/src/main/java/de/blight/common/LightIO.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 0–255. */
|
||||
/** 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 0–255. */
|
||||
/** 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 0–255 (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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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 : ""; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.blight.common;
|
||||
|
||||
public record PlacedMusicArea(
|
||||
float[] pointsX,
|
||||
float[] pointsZ,
|
||||
String dayTrack,
|
||||
String nightTrack,
|
||||
String combatTrack
|
||||
) {}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.blight.common;
|
||||
|
||||
public record PlacedSoundArea(
|
||||
float[] pointsX,
|
||||
float[] pointsZ,
|
||||
String soundPath,
|
||||
float volume,
|
||||
boolean crossfade
|
||||
) {}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface AudioReference {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface Interactable {
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
20
blight-common/src/main/java/de/blight/common/model/Item.java
Normal file
20
blight-common/src/main/java/de/blight/common/model/Item.java
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public enum ItemCategory {
|
||||
|
||||
WEAPON,
|
||||
GEAR,
|
||||
CONSUMABLES,
|
||||
QUEST_ITEMS,
|
||||
USABLES,
|
||||
MISC;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface ItemCount {
|
||||
|
||||
public Item getItem();
|
||||
public int getCount();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface Location {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
65
blight-common/src/main/java/de/blight/common/model/NPC.java
Normal file
65
blight-common/src/main/java/de/blight/common/model/NPC.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public interface ObjectReference {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.blight.common.model;
|
||||
|
||||
public record TextReference(String id) {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.blight.common.model.quests;
|
||||
|
||||
public interface QuestType {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.blight.common.time;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verwaltet die Spielzeit innerhalb eines Tages (0.0–1.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.0–1.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 (0–23). */
|
||||
public int getHour() { return (int)(timeOfDay * 24f); }
|
||||
|
||||
/** Aktuelle Spielminute (0–59). */
|
||||
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); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user