Weiter daran gearbeitet, die welt aus dem editor ins game zu bringen

This commit is contained in:
2026-05-23 09:07:44 +02:00
parent f9a77cc321
commit 728f506e97
36 changed files with 3728 additions and 77 deletions

View File

@@ -8,9 +8,12 @@ plugins {
group = 'de.blight' group = 'de.blight'
version = '0.1.0' version = '0.1.0'
// Explizite Toolchain für standalone-Import (Eclipse ohne Root-Build).
// Im Full-Build wird dies durch die Root-Konfiguration überschrieben.
java { java {
sourceCompatibility = JavaVersion.VERSION_21 toolchain {
targetCompatibility = JavaVersion.VERSION_21 languageVersion = JavaLanguageVersion.of(21)
}
} }
compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8'

View File

@@ -8,7 +8,7 @@ import java.util.zip.*;
/** /**
* Liest und schreibt {@link MapData} als komprimierte Binärdatei. * Liest und schreibt {@link MapData} als komprimierte Binärdatei.
* *
* Speicherort: {@code world/blight_map.blm} relativ zum Arbeitsverzeichnis. * Speicherort: {@code blight-map/src/main/map/blight_map.blm} relativ zum Arbeitsverzeichnis.
* Beide Projekte setzen {@code workingDir = rootDir} im Gradle-Run-Task, * Beide Projekte setzen {@code workingDir = rootDir} im Gradle-Run-Task,
* zeigen also auf dasselbe Verzeichnis. * zeigen also auf dasselbe Verzeichnis.
* *
@@ -19,7 +19,32 @@ import java.util.zip.*;
*/ */
public final class MapIO { public final class MapIO {
private static final Path MAP_PATH = Paths.get("world", "blight_map.blm"); private static final Path PROJECT_ROOT = findProjectRoot();
private static final Path MAP_PATH = PROJECT_ROOT.resolve(
Paths.get("blight-map", "src", "main", "map", "blight_map.blm"));
private static final Path MAP_PATH_OLD = PROJECT_ROOT.resolve(
Paths.get("world", "blight_map.blm"));
/**
* Ermittelt den Projekt-Root einmalig beim Classload:
* 1. System-Property {@code blight.project.root} (vom Editor-Subprocess gesetzt)
* 2. Aufwärtssuche im Verzeichnisbaum nach {@code gradlew}
* 3. Fallback: aktuelles Arbeitsverzeichnis
*/
private static Path findProjectRoot() {
String prop = System.getProperty("blight.project.root");
if (prop != null) return Paths.get(prop);
// blight-editor/ UND blight-game/ als Kinder → eindeutig der echte Root
File dir = Paths.get(".").toAbsolutePath().normalize().toFile();
while (dir != null) {
if (new File(dir, "blight-editor").isDirectory()
&& new File(dir, "blight-game").isDirectory())
return dir.toPath();
dir = dir.getParentFile();
}
return Paths.get(".").toAbsolutePath().normalize();
}
private static final int MAGIC = 0x424C4947; // "BLIG" private static final int MAGIC = 0x424C4947; // "BLIG"
private static final int VERSION = 3; private static final int VERSION = 3;
@@ -29,7 +54,20 @@ public final class MapIO {
// ── Public API ──────────────────────────────────────────────────────────── // ── Public API ────────────────────────────────────────────────────────────
public static boolean exists() { public static boolean exists() {
return Files.exists(MAP_PATH); System.out.println("[MapIO] Suche Karte: " + MAP_PATH.toAbsolutePath());
if (Files.exists(MAP_PATH)) return true;
// Einmalige Migration vom alten Speicherort (world/blight_map.blm)
if (Files.exists(MAP_PATH_OLD)) {
try {
Files.createDirectories(MAP_PATH.getParent());
Files.move(MAP_PATH_OLD, MAP_PATH);
System.out.println("[MapIO] Karte migriert: " + MAP_PATH_OLD + "" + MAP_PATH);
return true;
} catch (IOException e) {
System.err.println("[MapIO] Migration fehlgeschlagen: " + e.getMessage());
}
}
return false;
} }
public static Path getMapPath() { public static Path getMapPath() {

Binary file not shown.

View File

@@ -25,7 +25,11 @@ ext {
dependencies { dependencies {
implementation project(':blight-common') implementation project(':blight-common')
implementation project(':blight-assets') implementation project(':blight-assets')
implementation project(':blight-map')
implementation project(':ez-tree-jme') implementation project(':ez-tree-jme')
// Spiel-Klassen + deren Abhängigkeiten (jme3-jbullet, gson) auf dem Runtime-
// Classpath, damit der Editor BlightApp als Subprocess starten kann.
implementation project(':blight-game')
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}" implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}" implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"

Binary file not shown.

View File

@@ -0,0 +1,23 @@
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007c37e02a43a0, pid=83128, tid=83197
#
# JRE version: Java(TM) SE Runtime Environment (26.0.1+8) (build 26.0.1+8-34)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (26.0.1+8-34, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
# Problematic frame:
# C 0x00007c37e02a43a0
#
# Core dump will be written. Default location: Determined by the following: "/usr/lib/systemd/systemd-coredump %P %u %g %s %t 9223372036854775808 %h %d" (alternatively, falling back to /home/mario/Workspaces/blight/blight-editor/core.83128)
#
# If you would like to submit a bug report, please visit:
# https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
--------------- S U M M A R Y ------------
Command Line: -Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ShowCodeDetailsInExceptionMessages de.blight.editor.EditorLauncher
Host: Intel(R) Core(TM) i7-14700KF, 28 cores, 31G,

File diff suppressed because one or more lines are too long

View File

@@ -44,12 +44,17 @@ public class EditorApp extends Application {
static final int VP_WIDTH = 1024; static final int VP_WIDTH = 1024;
static final int VP_HEIGHT = 640; static final int VP_HEIGHT = 640;
private static final Path ASSET_ROOT = Paths.get("editor-assets"); private static final Path ASSET_ROOT = ProjectRoot.resolve("editor-assets");
private final SharedInput input = new SharedInput(); private final SharedInput input = new SharedInput();
private WritableImage jfxImage; private WritableImage jfxImage;
private ImageView viewport; private ImageView viewport;
private Label statusLabel; private Label statusLabel;
private Label camCoordsLabel;
private HBox consoleBar;
private TextField consoleField;
private boolean consoleOpen = false;
private boolean launchGameAfterSave = false;
private VBox toolPanel; private VBox toolPanel;
private BorderPane root; private BorderPane root;
private VBox assetPanel; private VBox assetPanel;
@@ -129,7 +134,7 @@ public class EditorApp extends Application {
root.setLeft(assetPanel); root.setLeft(assetPanel);
root.setCenter(worldViewport); root.setCenter(worldViewport);
root.setRight(toolPanel); root.setRight(toolPanel);
root.setBottom(buildStatusBar()); root.setBottom(buildBottomBox());
Scene scene = new Scene(root, 1280, 760); Scene scene = new Scene(root, 1280, 760);
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true)); scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
@@ -140,14 +145,21 @@ public class EditorApp extends Application {
stage.setScene(scene); stage.setScene(scene);
stage.setMinWidth(900); stage.setMinWidth(900);
stage.setMinHeight(600); stage.setMinHeight(600);
stage.setOnCloseRequest(e -> Platform.exit()); stage.setOnCloseRequest(e -> { saveCameraPrefs(); Platform.exit(); });
stage.setMaximized(true); stage.setMaximized(true);
stage.show(); stage.show();
javafx.animation.Timeline statusPoller = new javafx.animation.Timeline( javafx.animation.Timeline statusPoller = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
String saveMsg = input.saveStatusMsg; String saveMsg = input.saveStatusMsg;
if (saveMsg != null) { input.saveStatusMsg = null; setStatus(saveMsg); } if (saveMsg != null) {
input.saveStatusMsg = null;
setStatus(saveMsg);
if (launchGameAfterSave) {
launchGameAfterSave = false;
startGameProcess();
}
}
String treeMsg = input.treeGenStatusMsg; String treeMsg = input.treeGenStatusMsg;
if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); } if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); }
@@ -178,10 +190,31 @@ public class EditorApp extends Application {
input.objectSelectionChanged = false; input.objectSelectionChanged = false;
updateObjectPanel(input.selectedObjectInfo); updateObjectPanel(input.selectedObjectInfo);
} }
// Kamera-Koordinaten aktualisieren
camCoordsLabel.setText(String.format(
"X:%.1f Y:%.1f Z:%.1f Yaw:%.0f° Pitch:%.0f°",
input.camX, input.camY, input.camZ,
input.camYaw, input.camPitch));
// Konsolen-Antwort anzeigen
String consoleMsg = input.consoleOutput;
if (consoleMsg != null) { input.consoleOutput = null; setStatus(consoleMsg); }
}) })
); );
statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE); statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE);
statusPoller.play(); statusPoller.play();
javafx.animation.Timeline autoSave = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.seconds(60), ev -> {
if (!input.saveRequested) {
input.saveRequested = true;
setStatus("Auto-Speicherung…");
}
})
);
autoSave.setCycleCount(javafx.animation.Timeline.INDEFINITE);
autoSave.play();
} }
// ── Modus-Wechsel ──────────────────────────────────────────────────────── // ── Modus-Wechsel ────────────────────────────────────────────────────────
@@ -246,7 +279,7 @@ public class EditorApp extends Application {
ToolBar toolBar = new ToolBar(); ToolBar toolBar = new ToolBar();
ToggleButton baseBtn = new ToggleButton("▲▼ Basis-Terrain"); ToggleButton baseBtn = new ToggleButton("▲▼ Basis-Terrain");
ToggleButton upperBtn = new ToggleButton("Obere Schicht"); ToggleButton upperBtn = new ToggleButton("Gebirge");
ToggleButton holesBtn = new ToggleButton("⬤ Höhlen/Löcher"); ToggleButton holesBtn = new ToggleButton("⬤ Höhlen/Löcher");
ToggleButton grassBtn = new ToggleButton("🌿 Gras"); ToggleButton grassBtn = new ToggleButton("🌿 Gras");
ToggleButton textureBtn = new ToggleButton("🎨 Textur"); ToggleButton textureBtn = new ToggleButton("🎨 Textur");
@@ -305,17 +338,28 @@ public class EditorApp extends Application {
root.setRight(buildObjectEditPanel()); root.setRight(buildObjectEditPanel());
}); });
CheckBox visibleCB = new CheckBox("Obere Schicht sichtbar"); CheckBox visibleCB = new CheckBox("Gebirge sichtbar");
visibleCB.setSelected(true); visibleCB.setSelected(true);
visibleCB.setOnAction(e -> input.upperLayerVisible = visibleCB.isSelected()); visibleCB.setOnAction(e -> input.upperLayerVisible = visibleCB.isSelected());
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief"); Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
hint.setStyle("-fx-text-fill: #555;"); hint.setStyle("-fx-text-fill: #555;");
Region toolbarSpacer = new Region();
HBox.setHgrow(toolbarSpacer, Priority.ALWAYS);
Button playBtn = new Button("▶ Spielen");
playBtn.setStyle(
"-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
"-fx-font-weight: bold; -fx-padding: 4 12 4 12;");
playBtn.setOnAction(e -> launchGame());
toolBar.getItems().addAll(baseBtn, upperBtn, holesBtn, grassBtn, textureBtn, toolBar.getItems().addAll(baseBtn, upperBtn, holesBtn, grassBtn, textureBtn,
new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn, new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn,
new Separator(Orientation.VERTICAL), visibleCB, new Separator(Orientation.VERTICAL), visibleCB,
new Separator(Orientation.VERTICAL), hint); new Separator(Orientation.VERTICAL), hint,
toolbarSpacer,
new Separator(Orientation.VERTICAL), playBtn);
worldToolBar = toolBar; worldToolBar = toolBar;
return new VBox(menuBar, toolBar); return new VBox(menuBar, toolBar);
@@ -1819,13 +1863,118 @@ public class EditorApp extends Application {
// ── Statusleiste ───────────────────────────────────────────────────────── // ── Statusleiste ─────────────────────────────────────────────────────────
private HBox buildStatusBar() { private VBox buildBottomBox() {
// Status-Leiste
statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag / L+R-Drag: Drehen"); statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-Drag / L+R-Drag: Drehen");
statusLabel.setPadding(new Insets(3, 8, 3, 8)); statusLabel.setPadding(new Insets(3, 8, 3, 8));
statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #333;"); statusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #333;");
HBox bar = new HBox(statusLabel);
bar.setStyle("-fx-background-color: #e8e8e8; -fx-border-color: #bbb; -fx-border-width: 1 0 0 0;"); camCoordsLabel = new Label("X:0.0 Y:0.0 Z:0.0 Yaw:0° Pitch:0°");
return bar; camCoordsLabel.setPadding(new Insets(3, 8, 3, 8));
camCoordsLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555; -fx-font-family: monospace;");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
HBox statusBar = new HBox(statusLabel, spacer, camCoordsLabel);
statusBar.setStyle("-fx-background-color: #e8e8e8; -fx-border-color: #bbb; -fx-border-width: 1 0 0 0;");
// Konsolen-Panel (anfangs ausgeblendet)
Label prompt = new Label(">");
prompt.setStyle("-fx-text-fill: #7ec8e3; -fx-font-family: monospace; -fx-font-size: 12; -fx-padding: 0 4 0 0;");
consoleField = new TextField();
consoleField.setStyle(
"-fx-background-color: transparent; -fx-text-fill: #f0f0f0; " +
"-fx-font-family: monospace; -fx-font-size: 12; -fx-border-width: 0;");
consoleField.setPromptText("Befehl eingeben… (Enter = ausführen, Esc = schließen)");
HBox.setHgrow(consoleField, Priority.ALWAYS);
consoleField.setOnAction(e -> {
String cmd = consoleField.getText().trim();
if (!cmd.isEmpty()) input.pendingCommand = cmd;
toggleConsole();
});
consoleField.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ESCAPE
|| e.getCode() == KeyCode.DEAD_CIRCUMFLEX
|| e.getCode() == KeyCode.CIRCUMFLEX) {
toggleConsole();
e.consume();
}
});
consoleBar = new HBox(4, prompt, consoleField);
consoleBar.setStyle(
"-fx-background-color: #1e1e1e; -fx-padding: 3 8 3 8; " +
"-fx-border-color: #555; -fx-border-width: 1 0 0 0;");
consoleBar.setVisible(false);
consoleBar.setManaged(false);
return new VBox(statusBar, consoleBar);
}
private void toggleConsole() {
boolean show = !consoleOpen;
consoleOpen = show;
consoleBar.setVisible(show);
consoleBar.setManaged(show);
if (show) {
// Bewegungstasten loslassen, damit keine Dauerbewegung entsteht
input.forward = input.backward = input.left = input.right = input.up = input.down = false;
consoleField.clear();
consoleField.requestFocus();
}
}
private void launchGame() {
if (launchGameAfterSave) return; // bereits ausstehend
launchGameAfterSave = true;
input.saveRequested = true;
setStatus("Karte wird gespeichert, Spiel startet…");
}
private void startGameProcess() {
new Thread(() -> {
try {
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
String classpath = System.getProperty("java.class.path");
String libPath = System.getProperty("java.library.path", "");
String projRoot = ProjectRoot.PATH.toString();
new ProcessBuilder(
javaExe,
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
"-Djava.library.path=" + libPath,
"-Dblight.project.root=" + projRoot,
"-cp", classpath,
"de.blight.game.BlightApp")
.directory(ProjectRoot.PATH.toFile())
.inheritIO()
.start();
Platform.runLater(() -> setStatus("Spiel gestartet"));
} catch (IOException ex) {
Platform.runLater(() -> setStatus("Spielstart fehlgeschlagen: " + ex.getMessage()));
}
}, "game-launcher").start();
}
private void saveCameraPrefs() {
java.util.Properties p = new java.util.Properties();
p.setProperty("cam.x", String.valueOf(input.camX));
p.setProperty("cam.y", String.valueOf(input.camY));
p.setProperty("cam.z", String.valueOf(input.camZ));
p.setProperty("cam.yaw", String.valueOf(input.camYaw));
p.setProperty("cam.pitch", String.valueOf(input.camPitch));
try {
Files.createDirectories(ProjectRoot.resolve("config"));
try (java.io.Writer w = Files.newBufferedWriter(ProjectRoot.resolve("config", "editor.prefs"))) {
p.store(w, "Blight Editor Kamera-Einstellungen");
}
} catch (IOException e) {
System.err.println("[Editor] Kamera-Prefs konnten nicht gespeichert werden: " + e.getMessage());
}
} }
private void setStatus(String msg) { private void setStatus(String msg) {
@@ -1835,6 +1984,13 @@ public class EditorApp extends Application {
// ── Tastatur-Handling ──────────────────────────────────────────────────── // ── Tastatur-Handling ────────────────────────────────────────────────────
private void handleKeyPress(KeyCode code, boolean pressed) { private void handleKeyPress(KeyCode code, boolean pressed) {
// ^ öffnet/schließt die Konsole (DEAD_CIRCUMFLEX = deutsche Tastatur)
if (code == KeyCode.DEAD_CIRCUMFLEX || code == KeyCode.CIRCUMFLEX) {
if (pressed) toggleConsole();
return;
}
// Während die Konsole offen ist, keine Editor-Tasten weiterleiten
if (consoleOpen) return;
switch (code) { switch (code) {
case W -> input.forward = pressed; case W -> input.forward = pressed;
case S -> input.backward = pressed; case S -> input.backward = pressed;

View File

@@ -6,6 +6,10 @@ package de.blight.editor;
*/ */
public class EditorLauncher { public class EditorLauncher {
public static void main(String[] args) { public static void main(String[] args) {
// ProjectRoot muss als erstes initialisiert werden, damit alle
// relativen Pfade korrekt aufgelöst werden (auch bei IDE-Start mit
// workingDir = blight-editor/ statt Projekt-Root).
ProjectRoot.PATH.toString(); // Trigger static init
EditorApp.main(args); EditorApp.main(args);
} }
} }

View File

@@ -57,7 +57,7 @@ public class JmeEditorApp extends SimpleApplication {
// aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis). // aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis).
try { try {
assetManager.registerLocator( assetManager.registerLocator(
java.nio.file.Paths.get("editor-assets").toAbsolutePath().toString(), ProjectRoot.resolve("editor-assets").toAbsolutePath().toString(),
FileLocator.class); FileLocator.class);
} catch (Exception ignored) {} } catch (Exception ignored) {}
@@ -79,5 +79,64 @@ public class JmeEditorApp extends SimpleApplication {
} }
@Override @Override
public void simpleUpdate(float tpf) {} public void simpleUpdate(float tpf) {
com.jme3.math.Vector3f loc = cam.getLocation();
com.jme3.math.Vector3f dir = cam.getDirection();
input.camX = loc.x;
input.camY = loc.y;
input.camZ = loc.z;
input.camYaw = (float) Math.toDegrees(Math.atan2(-dir.x, -dir.z));
input.camPitch = (float) Math.toDegrees(
Math.asin(Math.max(-1f, Math.min(1f, dir.y))));
String cmd = input.pendingCommand;
if (cmd != null) {
input.pendingCommand = null;
processCommand(cmd);
}
}
private void processCommand(String raw) {
String[] parts = raw.trim().split("\\s+");
switch (parts[0].toLowerCase()) {
case "goto" -> {
try {
if (parts.length >= 4) {
// goto x y z — direkte Koordinaten
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
float z = Float.parseFloat(parts[3]);
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
input.consoleOutput = "Goto → " + x + " / " + y + " / " + z;
} else if (parts.length >= 3) {
// goto x z — Bodenabstand beibehalten
float x = Float.parseFloat(parts[1]);
float z = Float.parseFloat(parts[2]);
TerrainEditorState tes =
stateManager.getState(TerrainEditorState.class);
float srcGround = tes != null
? tes.getTerrainHeightAt(cam.getLocation().x,
cam.getLocation().z)
: 0f;
float heightAboveGround = cam.getLocation().y - srcGround;
float dstGround = tes != null
? tes.getTerrainHeightAt(x, z)
: 0f;
float y = dstGround + heightAboveGround;
cam.setLocation(new com.jme3.math.Vector3f(x, y, z));
input.consoleOutput = "Goto → X=" + x + " Z=" + z
+ " (Y=" + String.format("%.1f", y) + ")";
} else {
input.consoleOutput = "Syntax: goto <x> <z> oder goto <x> <y> <z>";
}
} catch (NumberFormatException e) {
input.consoleOutput = "Fehler: Koordinaten müssen Zahlen sein";
}
}
case "help" -> input.consoleOutput =
"Befehle: goto <x> <z> | goto <x> <y> <z> | help";
default ->
input.consoleOutput = "Unbekannter Befehl: " + parts[0];
}
}
} }

View File

@@ -0,0 +1,41 @@
package de.blight.editor;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Ermittelt einmalig den Projekt-Root (das Verzeichnis, das gradlew enthält)
* und stellt ihn als kanonischen Basispfad bereit.
*
* Muss so früh wie möglich aufgerufen werden (EditorLauncher.main),
* damit alle relativen Pfade korrekt aufgelöst werden — auch wenn der Editor
* aus einer IDE mit workingDir=blight-editor/ gestartet wird.
*/
public final class ProjectRoot {
public static final Path PATH = findRoot();
private ProjectRoot() {}
public static Path resolve(String first, String... more) {
return PATH.resolve(Paths.get(first, more));
}
private static Path findRoot() {
// blight-editor/ UND blight-game/ als Kinder → eindeutig der echte Root
File dir = Paths.get(".").toAbsolutePath().normalize().toFile();
while (dir != null) {
if (new File(dir, "blight-editor").isDirectory()
&& new File(dir, "blight-game").isDirectory()) {
Path root = dir.toPath();
System.setProperty("blight.project.root", root.toString());
return root;
}
dir = dir.getParentFile();
}
Path fallback = Paths.get(".").toAbsolutePath().normalize();
System.setProperty("blight.project.root", fallback.toString());
return fallback;
}
}

View File

@@ -24,7 +24,7 @@ public class SharedInput {
public final TextureTool textureTool = new TextureTool(); public final TextureTool textureTool = new TextureTool();
public volatile EditorTool activeTool = heightTool; public volatile EditorTool activeTool = heightTool;
// ── Aktive Ebene: 0=Basis-Terrain, 1=Obere Schicht, 2=Höhlen, 3=Gras, 4=Textur ── // ── Aktive Ebene: 0=Basis-Terrain, 1=Gebirge, 2=Höhlen, 3=Gras, 4=Textur ──
public volatile int activeLayer = 0; public volatile int activeLayer = 0;
public volatile boolean upperLayerVisible = true; public volatile boolean upperLayerVisible = true;
@@ -152,6 +152,19 @@ public class SharedInput {
public final ConcurrentLinkedQueue<MeshCreateRequest> meshCreateQueue = public final ConcurrentLinkedQueue<MeshCreateRequest> meshCreateQueue =
new ConcurrentLinkedQueue<>(); new ConcurrentLinkedQueue<>();
// ── Kamera-Info (JME3-Thread schreibt, JavaFX-Thread liest) ─────────────
public volatile float camX = 0f, camY = 0f, camZ = 0f;
/** Yaw in Grad: 0° = Süden (Z), 90° = Westen (X), ±180° = Norden (+Z). */
public volatile float camYaw = 0f;
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
public volatile float camPitch = 0f;
// ── Konsole (JavaFX → JME3 und zurück) ──────────────────────────────────
/** Befehl, der beim nächsten JME3-Update ausgeführt werden soll. */
public volatile String pendingCommand = null;
/** Antworttext, den JME3 nach der Befehlsausführung setzt. */
public volatile String consoleOutput = null;
// ── Modell-Konvertierung ────────────────────────────────────────────────── // ── Modell-Konvertierung ──────────────────────────────────────────────────
/** /**
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o. * Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.

View File

@@ -38,6 +38,8 @@ import java.nio.ByteBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/** /**
* JME3-AppState für den EZ-Tree-Generator. * JME3-AppState für den EZ-Tree-Generator.
@@ -50,7 +52,7 @@ import java.nio.file.Paths;
public class EzTreeState extends BaseAppState { public class EzTreeState extends BaseAppState {
private static final int IMPOSTOR_SIZE = 512; private static final int IMPOSTOR_SIZE = 512;
private static final Path ASSET_ROOT = Paths.get("editor-assets"); private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
private final SharedInput input; private final SharedInput input;
private SimpleApplication app; private SimpleApplication app;
@@ -155,8 +157,10 @@ public class EzTreeState extends BaseAppState {
app.getRenderer().readFrameBuffer(captureFB, pixels); app.getRenderer().readFrameBuffer(captureFB, pixels);
cleanupCapture(); cleanupCapture();
saveImpostor(pixels, "ez_impostor_" + pendingRequest.exportName()); String exportName = pendingRequest.exportName() + "_"
exportTree(pendingTreeNode, pendingRequest.exportName()); + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
saveImpostor(pixels, "ez_impostor_" + exportName);
exportTree(pendingTreeNode, exportName);
pendingRequest = null; pendingRequest = null;
pendingTreeNode = null; pendingTreeNode = null;

View File

@@ -24,10 +24,12 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class PalmGeneratorState extends BaseAppState { public class PalmGeneratorState extends BaseAppState {
private static final Path ASSET_ROOT = Paths.get("editor-assets"); private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
private final SharedInput input; private final SharedInput input;
private SimpleApplication app; private SimpleApplication app;
@@ -188,7 +190,9 @@ public class PalmGeneratorState extends BaseAppState {
try { try {
Path modelDir = ASSET_ROOT.resolve("models"); Path modelDir = ASSET_ROOT.resolve("models");
Files.createDirectories(modelDir); Files.createDirectories(modelDir);
File out = modelDir.resolve("Palm_" + name + ".j3o").toFile(); String stampedName = name + "_"
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
File out = modelDir.resolve("Palm_" + stampedName + ".j3o").toFile();
BinaryExporter.getInstance().save(palmNode, out); BinaryExporter.getInstance().save(palmNode, out);
input.treeGenStatusMsg = "Palme exportiert: " + out.getName(); input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
input.refreshAssets = true; input.refreshAssets = true;

View File

@@ -39,7 +39,7 @@ import java.util.List;
*/ */
public class SceneObjectState extends BaseAppState { public class SceneObjectState extends BaseAppState {
private static final Path ASSET_ROOT = Paths.get("editor-assets"); private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
// ── Gizmo-Farben ───────────────────────────────────────────────────────── // ── Gizmo-Farben ─────────────────────────────────────────────────────────
private static final ColorRGBA COL_X = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f); private static final ColorRGBA COL_X = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f);

View File

@@ -31,12 +31,18 @@ import de.blight.editor.SharedInput;
import de.blight.editor.tool.HeightTool; import de.blight.editor.tool.HeightTool;
import java.io.IOException; import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Properties;
public class TerrainEditorState extends BaseAppState { public class TerrainEditorState extends BaseAppState {
@@ -75,9 +81,13 @@ public class TerrainEditorState extends BaseAppState {
private Texture2D splatTex; private Texture2D splatTex;
// ── Kameraposition ──────────────────────────────────────────────────────── // ── Kameraposition ────────────────────────────────────────────────────────
private static final Path EDITOR_PREFS = de.blight.editor.ProjectRoot.resolve("config", "editor.prefs");
private static final float DEFAULT_CAM_Y = 50f;
private static final float DEFAULT_PITCH = (float) (-Math.PI / 4); // -45°
private float camYaw = 0f; private float camYaw = 0f;
private float camPitch = -1.0f; private float camPitch = DEFAULT_PITCH;
private final Vector3f camPos = new Vector3f(0f, 800f, (float)(800.0 / Math.tan(1.0))); private final Vector3f camPos = new Vector3f(0f, DEFAULT_CAM_Y, 0f);
public TerrainEditorState(SharedInput input) { public TerrainEditorState(SharedInput input) {
this.input = input; this.input = input;
@@ -101,6 +111,28 @@ public class TerrainEditorState extends BaseAppState {
System.err.println("[TerrainEditor] Karte nicht ladbar: " + e.getMessage()); System.err.println("[TerrainEditor] Karte nicht ladbar: " + e.getMessage());
} }
} }
loadCameraPrefs();
}
private void loadCameraPrefs() {
if (!Files.exists(EDITOR_PREFS)) return;
Properties p = new Properties();
try (Reader r = Files.newBufferedReader(EDITOR_PREFS)) {
p.load(r);
camPos.set(
parsePref(p, "cam.x", 0f),
parsePref(p, "cam.y", DEFAULT_CAM_Y),
parsePref(p, "cam.z", 0f));
camYaw = (float) Math.toRadians(parsePref(p, "cam.yaw", 0f));
camPitch = (float) Math.toRadians(parsePref(p, "cam.pitch", (float) Math.toDegrees(DEFAULT_PITCH)));
} catch (IOException e) {
System.err.println("[TerrainEditor] Kamera-Prefs nicht ladbar: " + e.getMessage());
}
}
private static float parsePref(Properties p, String key, float def) {
try { return Float.parseFloat(p.getProperty(key, String.valueOf(def))); }
catch (NumberFormatException e) { return def; }
} }
@Override @Override
@@ -354,6 +386,13 @@ public class TerrainEditorState extends BaseAppState {
} }
} }
/** Gibt die Terrain-Höhe (Welt-Y) an der angegebenen Welt-XZ-Position zurück. */
public float getTerrainHeightAt(float worldX, float worldZ) {
if (terrain == null) return 0f;
Float h = terrain.getHeight(new Vector2f(worldX, worldZ));
return h != null ? h : 0f;
}
// ── Speichern ───────────────────────────────────────────────────────────── // ── Speichern ─────────────────────────────────────────────────────────────
private void performSave() { private void performSave() {

View File

@@ -7,6 +7,8 @@ import java.nio.ByteBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -64,7 +66,7 @@ public class TreeGeneratorState extends BaseAppState {
private static final int IMPOSTOR_SIZE = 512; private static final int IMPOSTOR_SIZE = 512;
private static final int PREVIEW_SIZE = 1024; private static final int PREVIEW_SIZE = 1024;
private static final Path ASSET_ROOT = Paths.get("editor-assets"); private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("editor-assets");
private final SharedInput input; private final SharedInput input;
@@ -286,7 +288,12 @@ public class TreeGeneratorState extends BaseAppState {
app.getRenderer().readFrameBuffer(captureFB, pixels); app.getRenderer().readFrameBuffer(captureFB, pixels);
cleanupCapture(); cleanupCapture();
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + pendingRequest.exportName()); String baseName = pendingRequest.exportName();
String exportName = pendingRequest.exportAfter()
? baseName + "_" + DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now())
: baseName;
Texture2D impostorTex = saveImpostor(pixels, "impostor_" + exportName);
// HD-Mesh im Dialog-Preview anzeigen (keine LOD-Umschaltung, kein Welt-Platzierung) // HD-Mesh im Dialog-Preview anzeigen (keine LOD-Umschaltung, kein Welt-Platzierung)
Node previewTree = makeTreeNode(pendingHdResult, Node previewTree = makeTreeNode(pendingHdResult,
@@ -301,9 +308,9 @@ public class TreeGeneratorState extends BaseAppState {
if (pendingRequest.exportAfter()) { if (pendingRequest.exportAfter()) {
Node treeNode = assembleLodNode(impostorTex); Node treeNode = assembleLodNode(impostorTex);
exportTree(treeNode, pendingRequest.exportName()); exportTree(treeNode, exportName);
} else { } else {
input.treeGenStatusMsg = "Vorschau: '" + pendingRequest.exportName() + "'"; input.treeGenStatusMsg = "Vorschau: '" + baseName + "'";
} }
pendingRequest = null; pendingRequest = null;

View File

@@ -23,8 +23,9 @@ public class UpperLayerData {
/** Whether a cell is an open hole (no geometry emitted) [CELLS*CELLS]. */ /** Whether a cell is an open hole (no geometry emitted) [CELLS*CELLS]. */
public final boolean[] hole; public final boolean[] hole;
/** Initiale Höhe der Gras-Oberfläche (muss mit TerrainEditorState übereinstimmen). */ /** Initiale Höhe der Gebirgsschicht-Oberseite. Tiefer als das flache Terrain (Y=1),
public static final float INITIAL_TERRAIN_Y = 1f; * damit das Gebirge standardmäßig verdeckt ist und gezielt hochgezogen werden muss. */
public static final float INITIAL_TERRAIN_Y = -10f;
/** Dicke der Gesteinsschicht in Welteinheiten. */ /** Dicke der Gesteinsschicht in Welteinheiten. */
public static final float LAYER_THICKNESS = 30f; public static final float LAYER_THICKNESS = 30f;

View File

@@ -17,8 +17,9 @@ import de.blight.editor.SharedInput;
import de.blight.editor.tool.HoleTool; import de.blight.editor.tool.HoleTool;
import de.blight.editor.tool.UpperHeightTool; import de.blight.editor.tool.UpperHeightTool;
import java.util.HashSet; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* AppState that owns the upper (mountain) layer: 1024 chunk geometries * AppState that owns the upper (mountain) layer: 1024 chunk geometries
@@ -256,20 +257,27 @@ public class UpperLayerState extends BaseAppState {
/** /**
* Verschiebt top- und bottomHeight um dieselben Deltas wie das Basis-Terrain. * Verschiebt top- und bottomHeight um dieselben Deltas wie das Basis-Terrain.
* Jeder obere Vertex wird dabei nur einmal angepasst, auch wenn mehrere * Da das Terrain-Grid (4097×4097) feiner ist als das Gebirge-Grid (513×513),
* Terrain-Vertices auf denselben oberen Vertex fallen. * fallen mehrere Terrain-Vertices auf denselben Gebirge-Vertex. Wir nehmen
* jeweils den Delta mit dem größten Absolutbetrag, damit der Gebirge-Vertex
* der stärksten Terrain-Verschiebung folgt und das Terrain nicht durchsticht.
*/ */
public void adjustHeightsWithTerrain(List<Vector2f> worldXZ, List<Float> deltas) { public void adjustHeightsWithTerrain(List<Vector2f> worldXZ, List<Float> deltas) {
HashSet<Integer> seen = new HashSet<>(); HashMap<Integer, Float> maxDelta = new HashMap<>();
for (int i = 0; i < worldXZ.size(); i++) { for (int i = 0; i < worldXZ.size(); i++) {
Vector2f p = worldXZ.get(i); int uvx = UpperLayerData.worldToVertexX(worldXZ.get(i).x);
int uvx = UpperLayerData.worldToVertexX(p.x); int uvz = UpperLayerData.worldToVertexZ(worldXZ.get(i).y);
int uvz = UpperLayerData.worldToVertexZ(p.y);
int key = uvx + uvz * UpperLayerData.VERTS; int key = uvx + uvz * UpperLayerData.VERTS;
if (!seen.add(key)) continue;
float d = deltas.get(i); float d = deltas.get(i);
maxDelta.merge(key, d, (a, b) -> Math.abs(b) > Math.abs(a) ? b : a);
}
for (Map.Entry<Integer, Float> e : maxDelta.entrySet()) {
int key = e.getKey();
float d = e.getValue();
data.topHeight[key] += d; data.topHeight[key] += d;
data.bottomHeight[key] += d; data.bottomHeight[key] += d;
int uvx = key % UpperLayerData.VERTS;
int uvz = key / UpperLayerData.VERTS;
markVertexDirty(uvx, uvz); markVertexDirty(uvx, uvz);
} }
} }

View File

@@ -23,7 +23,7 @@ public class UpperHeightTool extends EditorTool {
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0); public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
@Override @Override
public String getName() { return "Obere Schicht Höhe"; } public String getName() { return "Gebirge Höhe"; }
@Override @Override
public List<ChoiceToolParameter> getChoiceParameters() { public List<ChoiceToolParameter> getChoiceParameters() {

Binary file not shown.

View File

@@ -1,16 +1,11 @@
// group / version / java / repositories kommen vom Root-Build. // group / version / java / repositories kommen vom Root-Build.
// Kein 'application'-Plugin: dessen DistributionPlugin nutzt afterEvaluate,
// was mit runtimeOnly project(':blight-game') in blight-editor kollidiert.
plugins { plugins {
id 'application' id 'java'
} }
application { ext { mainClassName = 'de.blight.game.BlightApp' }
mainClass = 'de.blight.game.BlightApp'
applicationDefaultJvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
"-Djava.library.path=${buildDir}/natives"
]
}
ext { ext {
jmeVersion = '3.9.0-stable' jmeVersion = '3.9.0-stable'
@@ -19,6 +14,7 @@ ext {
dependencies { dependencies {
implementation project(':blight-common') implementation project(':blight-common')
implementation project(':blight-assets') implementation project(':blight-assets')
implementation project(':blight-map')
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}" implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}" implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
@@ -41,13 +37,22 @@ tasks.register('extractNatives', Copy) {
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
} }
run { tasks.register('run', JavaExec) {
group = 'application'
description = 'Startet das Spiel'
mainClass = mainClassName
classpath = sourceSets.main.runtimeClasspath
jvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
"-Djava.library.path=${buildDir}/natives",
]
dependsOn extractNatives dependsOn extractNatives
workingDir = rootDir // gemeinsames Arbeitsverzeichnis = Projekt-Root workingDir = rootDir
} }
jar { jar {
manifest { manifest {
attributes 'Main-Class': application.mainClass attributes 'Main-Class': mainClassName
} }
} }

View File

@@ -6,19 +6,24 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.bullet.BulletAppState; import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.HeightfieldCollisionShape;
import com.jme3.bullet.control.CharacterControl; import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.light.*; import com.jme3.light.*;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*; import com.jme3.scene.*;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.*; import com.jme3.scene.shape.*;
import com.jme3.shadow.*; import com.jme3.shadow.*;
import com.jme3.terrain.geomipmap.*; import com.jme3.terrain.geomipmap.*;
import com.jme3.texture.*; import com.jme3.texture.*;
import com.jme3.util.BufferUtils;
import com.jme3.util.SkyFactory; import com.jme3.util.SkyFactory;
import java.nio.ByteBuffer;
import de.blight.common.MapData; import de.blight.common.MapData;
import de.blight.common.MapIO; import de.blight.common.MapIO;
import de.blight.game.config.KeyBindings; import de.blight.game.config.KeyBindings;
@@ -27,6 +32,8 @@ import de.blight.game.control.ThirdPersonCamera;
import de.blight.game.state.GrassState; import de.blight.game.state.GrassState;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class WorldScene extends BaseAppState { public class WorldScene extends BaseAppState {
@@ -73,9 +80,9 @@ public class WorldScene extends BaseAppState {
protected void onEnable() { protected void onEnable() {
buildLighting(); buildLighting();
TerrainQuad terrain = buildTerrain(); TerrainQuad terrain = buildTerrain();
buildDecorations(terrain);
if (loadedMapData != null) { if (loadedMapData != null) {
rootNode.attachChild(buildGebirge(loadedMapData));
app.getStateManager().attach(new GrassState(loadedMapData, terrain)); app.getStateManager().attach(new GrassState(loadedMapData, terrain));
} }
@@ -181,23 +188,31 @@ public class WorldScene extends BaseAppState {
} }
} }
// Höhe in der Weltmitte als Spawn-Grundlage // Spawn über dem höchsten Punkt Basis-Terrain UND Gebirge-Oberkante
float centerHeight = heights[(GAME_VERTS / 2) * GAME_VERTS + (GAME_VERTS / 2)]; float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
spawnY = centerHeight + 3f; for (float h : heights) { if (h < minH) minH = h; if (h > maxH) maxH = h; }
float midH = (minH + maxH) * 0.5f;
float maxUpperTop = maxH;
for (float h : map.upperTop) { if (h > maxUpperTop) maxUpperTop = h; }
spawnY = maxUpperTop + 20f;
TerrainQuad terrain = new TerrainQuad("terrain", 65, GAME_VERTS, heights); TerrainQuad terrain = new TerrainQuad("terrain", 65, GAME_VERTS, heights);
terrain.setLocalScale(8f, 1f, 8f); // 512 Zellen * 8 WE = 4096 WE pro Achse terrain.setLocalScale(8f, 1f, 8f);
terrain.setShadowMode(RenderQueue.ShadowMode.Receive); terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
applyTerrainMaterial(terrain, map);
rootNode.attachChild(terrain);
applyTerrainMaterial(terrain, 32f); // jBullet subtrahiert midH intern in getVertex() → Physics-Body bei midH
// damit Kollisionsfläche und sichtbares Terrain übereinstimmen.
RigidBodyControl terrainPhysics = new RigidBodyControl( HeightfieldCollisionShape hcs = new HeightfieldCollisionShape(
CollisionShapeFactory.createMeshShape(terrain), 0f); heights, terrain.getLocalScale());
RigidBodyControl terrainPhysics = new RigidBodyControl(hcs, 0f);
terrain.addControl(terrainPhysics); terrain.addControl(terrainPhysics);
bulletAppState.getPhysicsSpace().add(terrainPhysics); bulletAppState.getPhysicsSpace().add(terrainPhysics);
terrainPhysics.setPhysicsLocation(new Vector3f(0f, midH, 0f));
rootNode.attachChild(terrain); System.out.println("[WorldScene] Karte geladen, Spawn Y=" + spawnY
System.out.println("[WorldScene] Karte geladen, Spawn Y=" + spawnY); + " maxGebirgeH=" + maxUpperTop);
return terrain; return terrain;
} }
@@ -223,32 +238,234 @@ public class WorldScene extends BaseAppState {
terrain.setLocalScale(0.5f, 0.5f, 0.5f); terrain.setLocalScale(0.5f, 0.5f, 0.5f);
terrain.setShadowMode(RenderQueue.ShadowMode.Receive); terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
applyTerrainMaterial(terrain, 64f); applyTerrainMaterial(terrain, null);
rootNode.attachChild(terrain);
RigidBodyControl terrainPhysics = new RigidBodyControl( RigidBodyControl terrainPhysics = new RigidBodyControl(
CollisionShapeFactory.createMeshShape(terrain), 0f); CollisionShapeFactory.createMeshShape(terrain), 0f);
terrain.addControl(terrainPhysics); terrain.addControl(terrainPhysics);
bulletAppState.getPhysicsSpace().add(terrainPhysics); bulletAppState.getPhysicsSpace().add(terrainPhysics);
rootNode.attachChild(terrain);
return terrain; return terrain;
} }
private void applyTerrainMaterial(TerrainQuad terrain, float texScale) { // -----------------------------------------------------------------------
Material mat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md"); // Gebirge (obere Gesteinsschicht)
// -----------------------------------------------------------------------
private Node buildGebirge(MapData map) {
final int VERTS = 513;
final int CELLS = 512;
final float CELL = 8f;
final float ORIGIN = -2048f;
final int CHUNK = 16;
final int NCHUNK = CELLS / CHUNK;
Node node = new Node("gebirge");
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
try { try {
Texture rock = assetManager.loadTexture("Textures/Terrain/Rock2/rock.jpg");
rock.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("DiffuseMap", rock);
mat.setColor("Diffuse", new ColorRGBA(0.45f, 0.32f, 0.25f, 1f));
} catch (Exception e) {
mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", new ColorRGBA(0.18f, 0.12f, 0.08f, 1f));
}
mat.setColor("Ambient", new ColorRGBA(0.10f, 0.07f, 0.05f, 1f));
mat.setColor("Specular", new ColorRGBA(0.06f, 0.05f, 0.04f, 1f));
mat.setFloat("Shininess", 6f);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.getAdditionalRenderState().setPolyOffset(2f, 2f);
for (int chunkZ = 0; chunkZ < NCHUNK; chunkZ++) {
for (int chunkX = 0; chunkX < NCHUNK; chunkX++) {
List<Float> pos = new ArrayList<>();
List<Float> norm = new ArrayList<>();
List<Float> uv = new ArrayList<>();
List<Integer> idx = new ArrayList<>();
int vtx = 0;
for (int lcz = 0; lcz < CHUNK; lcz++) {
int cz = chunkZ * CHUNK + lcz;
for (int lcx = 0; lcx < CHUNK; lcx++) {
int cx = chunkX * CHUNK + lcx;
if (map.upperHole[cz * CELLS + cx] != 0) continue;
float wx0 = ORIGIN + cx * CELL, wx1 = wx0 + CELL;
float wz0 = ORIGIN + cz * CELL, wz1 = wz0 + CELL;
float t00 = map.upperTop[cz*VERTS+cx], t10 = map.upperTop[cz*VERTS+cx+1];
float t01 = map.upperTop[(cz+1)*VERTS+cx], t11 = map.upperTop[(cz+1)*VERTS+cx+1];
float b00 = map.upperBottom[cz*VERTS+cx], b10 = map.upperBottom[cz*VERTS+cx+1];
float b01 = map.upperBottom[(cz+1)*VERTS+cx], b11 = map.upperBottom[(cz+1)*VERTS+cx+1];
// UV: eine Textur-Kachel pro Zelle (8 WE)
float u0 = cx, u1 = cx+1f, v0 = cz, v1 = cz+1f;
// Oben (XZ-Ebene)
vtx = quad(pos,norm,uv,idx,vtx,
wx0,t00,wz0, wx1,t10,wz0, wx1,t11,wz1, wx0,t01,wz1, 0,1,0,
u0,v0, u1,v0, u1,v1, u0,v1);
// Unten
vtx = quad(pos,norm,uv,idx,vtx,
wx0,b00,wz0, wx0,b01,wz1, wx1,b11,wz1, wx1,b10,wz0, 0,-1,0,
u0,v0, u0,v1, u1,v1, u1,v0);
// Seiten UV: horizontal = Zellposition, vertikal = Höhe normiert
if (gebirgeHole(map,cx,cz-1,CELLS)) vtx = quad(pos,norm,uv,idx,vtx,
wx1,t10,wz0, wx0,t00,wz0, wx0,b00,wz0, wx1,b10,wz0, 0,0,-1,
u1,t10, u0,t00, u0,b00, u1,b10);
if (gebirgeHole(map,cx,cz+1,CELLS)) vtx = quad(pos,norm,uv,idx,vtx,
wx0,t01,wz1, wx1,t11,wz1, wx1,b11,wz1, wx0,b01,wz1, 0,0,1,
u0,t01, u1,t11, u1,b11, u0,b01);
if (gebirgeHole(map,cx-1,cz,CELLS)) vtx = quad(pos,norm,uv,idx,vtx,
wx0,t00,wz0, wx0,t01,wz1, wx0,b01,wz1, wx0,b00,wz0, -1,0,0,
v0,t00, v1,t01, v1,b01, v0,b00);
if (gebirgeHole(map,cx+1,cz,CELLS)) vtx = quad(pos,norm,uv,idx,vtx,
wx1,t11,wz1, wx1,t10,wz0, wx1,b10,wz0, wx1,b11,wz1, 1,0,0,
v1,t11, v0,t10, v0,b10, v1,b11);
}
}
if (idx.isEmpty()) continue;
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(toFA(pos)));
mesh.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(toFA(norm)));
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(toFA(uv)));
mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(toIA(idx)));
mesh.updateBound();
mesh.updateCounts();
Geometry geom = new Geometry("gebirge_" + chunkX + "_" + chunkZ, mesh);
geom.setMaterial(mat);
geom.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
RigidBodyControl rbc = new RigidBodyControl(
CollisionShapeFactory.createMeshShape(geom), 0f);
geom.addControl(rbc);
bulletAppState.getPhysicsSpace().add(rbc);
node.attachChild(geom);
}
}
return node;
}
private boolean gebirgeHole(MapData map, int cx, int cz, int CELLS) {
if (cx < 0 || cx >= CELLS || cz < 0 || cz >= CELLS) return true;
return map.upperHole[cz * CELLS + cx] != 0;
}
/** Fügt ein Quad (2 Dreiecke) mit Position, Normal, UV und Index hinzu. */
private int quad(List<Float> pos, List<Float> norm, List<Float> uv, List<Integer> idx, int v,
float x0, float y0, float z0, float x1, float y1, float z1,
float x2, float y2, float z2, float x3, float y3, float z3,
float nx, float ny, float nz,
float u0, float v0, float u1, float v1,
float u2, float v2, float u3, float v3) {
pos.add(x0); pos.add(y0); pos.add(z0);
pos.add(x1); pos.add(y1); pos.add(z1);
pos.add(x2); pos.add(y2); pos.add(z2);
pos.add(x3); pos.add(y3); pos.add(z3);
for (int i = 0; i < 4; i++) { norm.add(nx); norm.add(ny); norm.add(nz); }
uv.add(u0); uv.add(v0);
uv.add(u1); uv.add(v1);
uv.add(u2); uv.add(v2);
uv.add(u3); uv.add(v3);
idx.add(v); idx.add(v+1); idx.add(v+2);
idx.add(v); idx.add(v+2); idx.add(v+3);
return v + 4;
}
private float[] toFA(List<Float> l) {
float[] a = new float[l.size()];
for (int i = 0; i < l.size(); i++) a[i] = l.get(i);
return a;
}
private int[] toIA(List<Integer> l) {
int[] a = new int[l.size()];
for (int i = 0; i < l.size(); i++) a[i] = l.get(i);
return a;
}
private void applyTerrainMaterial(TerrainQuad terrain, MapData map) {
if (map != null) {
try {
Material mat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
Texture tex1 = loadTexOrFallback("Textures/Terrain/splat/grass.jpg",
new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
Texture tex2 = loadTexOrFallback("Textures/Terrain/splat/road.jpg",
new ColorRGBA(0.55f, 0.50f, 0.40f, 1f));
Texture tex3 = loadTexOrFallback("Textures/Terrain/splat/Gravel.jpg",
new ColorRGBA(0.45f, 0.35f, 0.25f, 1f));
tex1.setWrap(Texture.WrapMode.Repeat);
tex2.setWrap(Texture.WrapMode.Repeat);
tex3.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("Tex1", tex1); mat.setFloat("Tex1Scale", 512f);
mat.setTexture("Tex2", tex2); mat.setFloat("Tex2Scale", 512f);
mat.setTexture("Tex3", tex3); mat.setFloat("Tex3Scale", 512f);
// Ältere Maps haben splatR=0 → Gras (Tex1) wäre unsichtbar; auf 255 setzen.
byte[] splatR = map.splatR;
boolean rAllZero = true;
for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } }
if (rAllZero) {
splatR = new byte[splatR.length];
java.util.Arrays.fill(splatR, (byte) 255);
}
int sz = MapData.SPLAT_SIZE;
ByteBuffer splatBuf = BufferUtils.createByteBuffer(sz * sz * 4);
for (int i = 0; i < sz * sz; i++) {
splatBuf.put(splatR[i]);
splatBuf.put(map.splatG[i]);
splatBuf.put(map.splatB[i]);
splatBuf.put((byte) 0);
}
splatBuf.flip();
Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf));
splatTex.setWrap(Texture.WrapMode.EdgeClamp);
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
splatTex.setMagFilter(Texture.MagFilter.Bilinear);
mat.setTexture("Alpha", splatTex);
terrain.setMaterial(mat);
return;
} catch (Exception e) {
System.err.println("[WorldScene] Splat-Material fehlgeschlagen: " + e.getMessage());
}
}
// Fallback: einfaches Gras-Material
try {
Material mat = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
Texture grass = assetManager.loadTexture("Textures/gras.png"); Texture grass = assetManager.loadTexture("Textures/gras.png");
grass.setWrap(Texture.WrapMode.Repeat); grass.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("Tex1", grass); mat.setTexture("DiffuseMap", grass);
mat.setFloat("Tex1Scale", texScale); mat.setFloat("DiffuseMap_0_scale", 32f);
mat.setBoolean("useTriPlanarMapping", false);
terrain.setMaterial(mat);
} catch (Exception e) { } catch (Exception e) {
// Fallback: einfarbiges Material Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
mat.setBoolean("UseMaterialColors", true); mat.setBoolean("UseMaterialColors", true);
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f)); mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
mat.setColor("Ambient", new ColorRGBA(0.15f, 0.30f, 0.09f, 1f)); mat.setColor("Ambient", new ColorRGBA(0.15f, 0.30f, 0.09f, 1f));
terrain.setMaterial(mat);
}
}
private Texture loadTexOrFallback(String path, ColorRGBA color) {
try {
return assetManager.loadTexture(path);
} catch (Exception e) {
ByteBuffer buf = BufferUtils.createByteBuffer(4);
buf.put((byte)(color.r * 255)).put((byte)(color.g * 255))
.put((byte)(color.b * 255)).put((byte)(color.a * 255));
buf.flip();
return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf));
} }
terrain.setMaterial(mat);
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

14
blight-map/build.gradle Normal file
View File

@@ -0,0 +1,14 @@
// Enthält die Karten-Daten des Spiels (src/main/map/blight_map.blm).
// Wird von blight-editor und blight-game importiert, damit beide
// denselben kanonischen Map-Pfad verwenden.
plugins {
id 'java'
}
sourceSets {
main {
resources {
srcDirs = ['src/main/map', 'src/main/resources']
}
}
}

View File

@@ -0,0 +1,13 @@
package de.blight.map;
import java.nio.file.Path;
import java.nio.file.Paths;
/** Kanonische Pfade zur Karten-Datei, relativ zum Projekt-Root (workingDir). */
public final class MapPaths {
public static final Path MAP_DIR = Paths.get("blight-map", "src", "main", "map");
public static final Path MAP_FILE = MAP_DIR.resolve("blight_map.blm");
private MapPaths() {}
}

View File

Binary file not shown.

View File

@@ -9,8 +9,11 @@ subprojects {
apply plugin: 'java' apply plugin: 'java'
java { java {
sourceCompatibility = JavaVersion.VERSION_26 // Toolchain: Source-Kompilierung mit Java 26, Gradle-Daemon läuft auf Java 21
targetCompatibility = JavaVersion.VERSION_26 // (siehe gradle.properties → org.gradle.java.home).
toolchain {
languageVersion = JavaLanguageVersion.of(26)
}
} }
compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8'

3
gradle.properties Normal file
View File

@@ -0,0 +1,3 @@
# Gradle-Daemon läuft auf Java 21 (Kompatibilität mit Groovy 3.x ASM).
# Source-Kompilierung erfolgt über Toolchain mit Java 26 (→ build.gradle).
org.gradle.java.home=/usr/lib/jvm/java-21-openjdk-amd64

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@@ -2,6 +2,7 @@ rootProject.name = 'blight'
include 'blight-common' include 'blight-common'
include 'blight-assets' include 'blight-assets'
include 'blight-map'
include 'blight-editor' include 'blight-editor'
include 'blight-game' include 'blight-game'
include 'simarboreal' include 'simarboreal'

Binary file not shown.