Weiter daran gearbeitet, die welt aus dem editor ins game zu bringen
This commit is contained in:
@@ -8,9 +8,12 @@ plugins {
|
||||
group = 'de.blight'
|
||||
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 {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.zip.*;
|
||||
/**
|
||||
* 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,
|
||||
* zeigen also auf dasselbe Verzeichnis.
|
||||
*
|
||||
@@ -19,7 +19,32 @@ import java.util.zip.*;
|
||||
*/
|
||||
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 VERSION = 3;
|
||||
@@ -29,7 +54,20 @@ public final class MapIO {
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
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() {
|
||||
|
||||
BIN
blight-editor/blight-map/src/main/map/blight_map.blm
Normal file
BIN
blight-editor/blight-map/src/main/map/blight_map.blm
Normal file
Binary file not shown.
@@ -25,7 +25,11 @@ ext {
|
||||
dependencies {
|
||||
implementation project(':blight-common')
|
||||
implementation project(':blight-assets')
|
||||
implementation project(':blight-map')
|
||||
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-desktop:${jmeVersion}"
|
||||
|
||||
BIN
blight-editor/editor-assets/models/Palm_Palme1.j3o
Normal file
BIN
blight-editor/editor-assets/models/Palm_Palme1.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
23
blight-editor/hs_err_pid83128.log
Normal file
23
blight-editor/hs_err_pid83128.log
Normal 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,
|
||||
2991
blight-editor/hs_err_pid83303.log
Normal file
2991
blight-editor/hs_err_pid83303.log
Normal file
File diff suppressed because one or more lines are too long
@@ -44,12 +44,17 @@ public class EditorApp extends Application {
|
||||
static final int VP_WIDTH = 1024;
|
||||
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 WritableImage jfxImage;
|
||||
private ImageView viewport;
|
||||
private Label statusLabel;
|
||||
private Label camCoordsLabel;
|
||||
private HBox consoleBar;
|
||||
private TextField consoleField;
|
||||
private boolean consoleOpen = false;
|
||||
private boolean launchGameAfterSave = false;
|
||||
private VBox toolPanel;
|
||||
private BorderPane root;
|
||||
private VBox assetPanel;
|
||||
@@ -129,7 +134,7 @@ public class EditorApp extends Application {
|
||||
root.setLeft(assetPanel);
|
||||
root.setCenter(worldViewport);
|
||||
root.setRight(toolPanel);
|
||||
root.setBottom(buildStatusBar());
|
||||
root.setBottom(buildBottomBox());
|
||||
|
||||
Scene scene = new Scene(root, 1280, 760);
|
||||
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
|
||||
@@ -140,14 +145,21 @@ public class EditorApp extends Application {
|
||||
stage.setScene(scene);
|
||||
stage.setMinWidth(900);
|
||||
stage.setMinHeight(600);
|
||||
stage.setOnCloseRequest(e -> Platform.exit());
|
||||
stage.setOnCloseRequest(e -> { saveCameraPrefs(); Platform.exit(); });
|
||||
stage.setMaximized(true);
|
||||
stage.show();
|
||||
|
||||
javafx.animation.Timeline statusPoller = new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
|
||||
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;
|
||||
if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); }
|
||||
@@ -178,10 +190,31 @@ public class EditorApp extends Application {
|
||||
input.objectSelectionChanged = false;
|
||||
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.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 ────────────────────────────────────────────────────────
|
||||
@@ -246,7 +279,7 @@ public class EditorApp extends Application {
|
||||
|
||||
ToolBar toolBar = new ToolBar();
|
||||
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 grassBtn = new ToggleButton("🌿 Gras");
|
||||
ToggleButton textureBtn = new ToggleButton("🎨 Textur");
|
||||
@@ -305,17 +338,28 @@ public class EditorApp extends Application {
|
||||
root.setRight(buildObjectEditPanel());
|
||||
});
|
||||
|
||||
CheckBox visibleCB = new CheckBox("Obere Schicht sichtbar");
|
||||
CheckBox visibleCB = new CheckBox("Gebirge sichtbar");
|
||||
visibleCB.setSelected(true);
|
||||
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");
|
||||
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,
|
||||
new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn,
|
||||
new Separator(Orientation.VERTICAL), visibleCB,
|
||||
new Separator(Orientation.VERTICAL), hint);
|
||||
new Separator(Orientation.VERTICAL), hint,
|
||||
toolbarSpacer,
|
||||
new Separator(Orientation.VERTICAL), playBtn);
|
||||
|
||||
worldToolBar = toolBar;
|
||||
return new VBox(menuBar, toolBar);
|
||||
@@ -1819,13 +1863,118 @@ public class EditorApp extends Application {
|
||||
|
||||
// ── 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.setPadding(new Insets(3, 8, 3, 8));
|
||||
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;");
|
||||
return bar;
|
||||
|
||||
camCoordsLabel = new Label("X:0.0 Y:0.0 Z:0.0 Yaw:0° Pitch:0°");
|
||||
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) {
|
||||
@@ -1835,6 +1984,13 @@ public class EditorApp extends Application {
|
||||
// ── Tastatur-Handling ────────────────────────────────────────────────────
|
||||
|
||||
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) {
|
||||
case W -> input.forward = pressed;
|
||||
case S -> input.backward = pressed;
|
||||
|
||||
@@ -6,6 +6,10 @@ package de.blight.editor;
|
||||
*/
|
||||
public class EditorLauncher {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
// aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis).
|
||||
try {
|
||||
assetManager.registerLocator(
|
||||
java.nio.file.Paths.get("editor-assets").toAbsolutePath().toString(),
|
||||
ProjectRoot.resolve("editor-assets").toAbsolutePath().toString(),
|
||||
FileLocator.class);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
@@ -79,5 +79,64 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
}
|
||||
|
||||
@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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class SharedInput {
|
||||
public final TextureTool textureTool = new TextureTool();
|
||||
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 boolean upperLayerVisible = true;
|
||||
|
||||
@@ -152,6 +152,19 @@ public class SharedInput {
|
||||
public final ConcurrentLinkedQueue<MeshCreateRequest> meshCreateQueue =
|
||||
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 ──────────────────────────────────────────────────
|
||||
/**
|
||||
* Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.
|
||||
|
||||
@@ -38,6 +38,8 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* JME3-AppState für den EZ-Tree-Generator.
|
||||
@@ -50,7 +52,7 @@ import java.nio.file.Paths;
|
||||
public class EzTreeState extends BaseAppState {
|
||||
|
||||
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 SimpleApplication app;
|
||||
@@ -155,8 +157,10 @@ public class EzTreeState extends BaseAppState {
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
cleanupCapture();
|
||||
|
||||
saveImpostor(pixels, "ez_impostor_" + pendingRequest.exportName());
|
||||
exportTree(pendingTreeNode, pendingRequest.exportName());
|
||||
String exportName = pendingRequest.exportName() + "_"
|
||||
+ DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
saveImpostor(pixels, "ez_impostor_" + exportName);
|
||||
exportTree(pendingTreeNode, exportName);
|
||||
|
||||
pendingRequest = null;
|
||||
pendingTreeNode = null;
|
||||
|
||||
@@ -24,10 +24,12 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
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 SimpleApplication app;
|
||||
@@ -188,7 +190,9 @@ public class PalmGeneratorState extends BaseAppState {
|
||||
try {
|
||||
Path modelDir = ASSET_ROOT.resolve("models");
|
||||
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);
|
||||
input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
|
||||
input.refreshAssets = true;
|
||||
|
||||
@@ -39,7 +39,7 @@ import java.util.List;
|
||||
*/
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
private static final ColorRGBA COL_X = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f);
|
||||
|
||||
@@ -31,12 +31,18 @@ import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.tool.HeightTool;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
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.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
@@ -75,9 +81,13 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private Texture2D splatTex;
|
||||
|
||||
// ── 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 camPitch = -1.0f;
|
||||
private final Vector3f camPos = new Vector3f(0f, 800f, (float)(800.0 / Math.tan(1.0)));
|
||||
private float camPitch = DEFAULT_PITCH;
|
||||
private final Vector3f camPos = new Vector3f(0f, DEFAULT_CAM_Y, 0f);
|
||||
|
||||
public TerrainEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
@@ -101,6 +111,28 @@ public class TerrainEditorState extends BaseAppState {
|
||||
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
|
||||
@@ -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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void performSave() {
|
||||
|
||||
@@ -7,6 +7,8 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@@ -64,7 +66,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
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;
|
||||
|
||||
@@ -286,7 +288,12 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
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)
|
||||
Node previewTree = makeTreeNode(pendingHdResult,
|
||||
@@ -301,9 +308,9 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
if (pendingRequest.exportAfter()) {
|
||||
Node treeNode = assembleLodNode(impostorTex);
|
||||
exportTree(treeNode, pendingRequest.exportName());
|
||||
exportTree(treeNode, exportName);
|
||||
} else {
|
||||
input.treeGenStatusMsg = "Vorschau: '" + pendingRequest.exportName() + "'";
|
||||
input.treeGenStatusMsg = "Vorschau: '" + baseName + "'";
|
||||
}
|
||||
|
||||
pendingRequest = null;
|
||||
|
||||
@@ -23,8 +23,9 @@ public class UpperLayerData {
|
||||
/** Whether a cell is an open hole (no geometry emitted) [CELLS*CELLS]. */
|
||||
public final boolean[] hole;
|
||||
|
||||
/** Initiale Höhe der Gras-Oberfläche (muss mit TerrainEditorState übereinstimmen). */
|
||||
public static final float INITIAL_TERRAIN_Y = 1f;
|
||||
/** Initiale Höhe der Gebirgsschicht-Oberseite. Tiefer als das flache Terrain (Y=1),
|
||||
* 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. */
|
||||
public static final float LAYER_THICKNESS = 30f;
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.tool.HoleTool;
|
||||
import de.blight.editor.tool.UpperHeightTool;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Jeder obere Vertex wird dabei nur einmal angepasst, auch wenn mehrere
|
||||
* Terrain-Vertices auf denselben oberen Vertex fallen.
|
||||
* Da das Terrain-Grid (4097×4097) feiner ist als das Gebirge-Grid (513×513),
|
||||
* 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) {
|
||||
HashSet<Integer> seen = new HashSet<>();
|
||||
HashMap<Integer, Float> maxDelta = new HashMap<>();
|
||||
for (int i = 0; i < worldXZ.size(); i++) {
|
||||
Vector2f p = worldXZ.get(i);
|
||||
int uvx = UpperLayerData.worldToVertexX(p.x);
|
||||
int uvz = UpperLayerData.worldToVertexZ(p.y);
|
||||
int uvx = UpperLayerData.worldToVertexX(worldXZ.get(i).x);
|
||||
int uvz = UpperLayerData.worldToVertexZ(worldXZ.get(i).y);
|
||||
int key = uvx + uvz * UpperLayerData.VERTS;
|
||||
if (!seen.add(key)) continue;
|
||||
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.bottomHeight[key] += d;
|
||||
int uvx = key % UpperLayerData.VERTS;
|
||||
int uvz = key / UpperLayerData.VERTS;
|
||||
markVertexDirty(uvx, uvz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public class UpperHeightTool extends EditorTool {
|
||||
public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
|
||||
|
||||
@Override
|
||||
public String getName() { return "Obere Schicht – Höhe"; }
|
||||
public String getName() { return "Gebirge – Höhe"; }
|
||||
|
||||
@Override
|
||||
public List<ChoiceToolParameter> getChoiceParameters() {
|
||||
|
||||
Binary file not shown.
@@ -1,16 +1,11 @@
|
||||
// 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 {
|
||||
id 'application'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
application {
|
||||
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 { mainClassName = 'de.blight.game.BlightApp' }
|
||||
|
||||
ext {
|
||||
jmeVersion = '3.9.0-stable'
|
||||
@@ -19,6 +14,7 @@ ext {
|
||||
dependencies {
|
||||
implementation project(':blight-common')
|
||||
implementation project(':blight-assets')
|
||||
implementation project(':blight-map')
|
||||
|
||||
implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
|
||||
@@ -41,13 +37,22 @@ tasks.register('extractNatives', Copy) {
|
||||
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
|
||||
workingDir = rootDir // gemeinsames Arbeitsverzeichnis = Projekt-Root
|
||||
workingDir = rootDir
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Main-Class': application.mainClass
|
||||
attributes 'Main-Class': mainClassName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,24 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
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.RigidBodyControl;
|
||||
import com.jme3.bullet.util.CollisionShapeFactory;
|
||||
import com.jme3.light.*;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.shape.*;
|
||||
import com.jme3.shadow.*;
|
||||
import com.jme3.terrain.geomipmap.*;
|
||||
import com.jme3.texture.*;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import com.jme3.util.SkyFactory;
|
||||
import java.nio.ByteBuffer;
|
||||
import de.blight.common.MapData;
|
||||
import de.blight.common.MapIO;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
@@ -27,6 +32,8 @@ import de.blight.game.control.ThirdPersonCamera;
|
||||
import de.blight.game.state.GrassState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WorldScene extends BaseAppState {
|
||||
|
||||
@@ -73,9 +80,9 @@ public class WorldScene extends BaseAppState {
|
||||
protected void onEnable() {
|
||||
buildLighting();
|
||||
TerrainQuad terrain = buildTerrain();
|
||||
buildDecorations(terrain);
|
||||
|
||||
if (loadedMapData != null) {
|
||||
rootNode.attachChild(buildGebirge(loadedMapData));
|
||||
app.getStateManager().attach(new GrassState(loadedMapData, terrain));
|
||||
}
|
||||
|
||||
@@ -181,23 +188,31 @@ public class WorldScene extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// Höhe in der Weltmitte als Spawn-Grundlage
|
||||
float centerHeight = heights[(GAME_VERTS / 2) * GAME_VERTS + (GAME_VERTS / 2)];
|
||||
spawnY = centerHeight + 3f;
|
||||
// Spawn über dem höchsten Punkt – Basis-Terrain UND Gebirge-Oberkante
|
||||
float minH = Float.MAX_VALUE, maxH = -Float.MAX_VALUE;
|
||||
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);
|
||||
terrain.setLocalScale(8f, 1f, 8f); // 512 Zellen * 8 WE = 4096 WE pro Achse
|
||||
terrain.setLocalScale(8f, 1f, 8f);
|
||||
terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
|
||||
applyTerrainMaterial(terrain, map);
|
||||
rootNode.attachChild(terrain);
|
||||
|
||||
applyTerrainMaterial(terrain, 32f);
|
||||
|
||||
RigidBodyControl terrainPhysics = new RigidBodyControl(
|
||||
CollisionShapeFactory.createMeshShape(terrain), 0f);
|
||||
// jBullet subtrahiert midH intern in getVertex() → Physics-Body bei midH
|
||||
// damit Kollisionsfläche und sichtbares Terrain übereinstimmen.
|
||||
HeightfieldCollisionShape hcs = new HeightfieldCollisionShape(
|
||||
heights, terrain.getLocalScale());
|
||||
RigidBodyControl terrainPhysics = new RigidBodyControl(hcs, 0f);
|
||||
terrain.addControl(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;
|
||||
}
|
||||
|
||||
@@ -223,32 +238,234 @@ public class WorldScene extends BaseAppState {
|
||||
terrain.setLocalScale(0.5f, 0.5f, 0.5f);
|
||||
terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
|
||||
|
||||
applyTerrainMaterial(terrain, 64f);
|
||||
applyTerrainMaterial(terrain, null);
|
||||
|
||||
rootNode.attachChild(terrain);
|
||||
|
||||
RigidBodyControl terrainPhysics = new RigidBodyControl(
|
||||
CollisionShapeFactory.createMeshShape(terrain), 0f);
|
||||
terrain.addControl(terrainPhysics);
|
||||
bulletAppState.getPhysicsSpace().add(terrainPhysics);
|
||||
|
||||
rootNode.attachChild(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 {
|
||||
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");
|
||||
grass.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture("Tex1", grass);
|
||||
mat.setFloat("Tex1Scale", texScale);
|
||||
mat.setTexture("DiffuseMap", grass);
|
||||
mat.setFloat("DiffuseMap_0_scale", 32f);
|
||||
mat.setBoolean("useTriPlanarMapping", false);
|
||||
terrain.setMaterial(mat);
|
||||
} catch (Exception e) {
|
||||
// Fallback: einfarbiges Material
|
||||
mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
|
||||
mat.setBoolean("UseMaterialColors", true);
|
||||
mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.58f, 0.18f, 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
14
blight-map/build.gradle
Normal 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']
|
||||
}
|
||||
}
|
||||
}
|
||||
13
blight-map/src/main/java/de/blight/map/MapPaths.java
Normal file
13
blight-map/src/main/java/de/blight/map/MapPaths.java
Normal 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() {}
|
||||
}
|
||||
0
blight-map/src/main/map/.gitkeep
Normal file
0
blight-map/src/main/map/.gitkeep
Normal file
BIN
blight-map/src/main/map/blight_map.blm
Normal file
BIN
blight-map/src/main/map/blight_map.blm
Normal file
Binary file not shown.
@@ -9,8 +9,11 @@ subprojects {
|
||||
apply plugin: 'java'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_26
|
||||
targetCompatibility = JavaVersion.VERSION_26
|
||||
// Toolchain: Source-Kompilierung mit Java 26, Gradle-Daemon läuft auf Java 21
|
||||
// (siehe gradle.properties → org.gradle.java.home).
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(26)
|
||||
}
|
||||
}
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
|
||||
3
gradle.properties
Normal file
3
gradle.properties
Normal 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
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -2,6 +2,7 @@ rootProject.name = 'blight'
|
||||
|
||||
include 'blight-common'
|
||||
include 'blight-assets'
|
||||
include 'blight-map'
|
||||
include 'blight-editor'
|
||||
include 'blight-game'
|
||||
include 'simarboreal'
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user