diff --git a/.gitignore b/.gitignore
index 398e814..6d729f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,74 +1,8 @@
-# ── Gradle ──────────────────────────────────────────────────────────────────
-.gradle/
-**/build/
-gradle-app.setting
-!gradle/wrapper/gradle-wrapper.jar
-!**/gradle/wrapper/gradle-wrapper.jar
+/.metadata/
+/.gradle/
-# ── Kompilate & Archive ──────────────────────────────────────────────────────
-*.class
-*.jar
-!gradle/wrapper/gradle-wrapper.jar
-!**/gradle/wrapper/gradle-wrapper.jar
-
-# ── Laufzeit-Konfiguration (benutzerspezifisch, nicht ins Repo) ──────────────
-/config/
-/blight-game/config/
-/blight-editor/config/
-/blight-editor/editor-assets/
-
-# ── JVM-Crashdumps & Logs ────────────────────────────────────────────────────
-hs_err_pid*.log
-replay_pid*.log
-*.log
-nohup.out
-
-# ── Native Bibliotheken (werden beim Build extrahiert) ───────────────────────
-*.so
-*.dll
-*.dylib
-*.jnilib
-
-# ── IntelliJ IDEA ────────────────────────────────────────────────────────────
-.idea/
-*.iml
-*.ipr
-*.iws
-out/
-
-# ── Eclipse ──────────────────────────────────────────────────────────────────
+# Eclipse project files
.project
.classpath
.settings/
-*.launch
-.factorypath
-
-# ── NetBeans ─────────────────────────────────────────────────────────────────
-nbproject/
-nbactions.xml
-nb-configuration.xml
-
-# ── VS Code ──────────────────────────────────────────────────────────────────
-.vscode/
-
-# ── macOS ────────────────────────────────────────────────────────────────────
-.DS_Store
-.AppleDouble
-.LSOverride
-
-# ── Windows ──────────────────────────────────────────────────────────────────
-Thumbs.db
-desktop.ini
-ehthumbs.db
-
-# ── Linux ────────────────────────────────────────────────────────────────────
-*~
-.fuse_hidden*
-
-# ── Editor-Backup & Temporärdateien ─────────────────────────────────────────
-*.swp
-*.swo
-*.tmp
-*.temp
-*.bak
-*.orig
+bin/
diff --git a/.metadata/.lock_info b/.metadata/.lock_info
index 02c94a1..0f01640 100644
--- a/.metadata/.lock_info
+++ b/.metadata/.lock_info
@@ -1,5 +1,4 @@
-#Thu May 07 06:27:32 CEST 2026
-display=\:0
-host=mario-mint
-process-id=5833
-user=mario
+#Sun May 10 10:13:47 CEST 2026
+host=BOOK-0F2PQ8OG21
+process-id=27380
+user=mario
diff --git a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json
index ac54edc..bb47060 100644
--- a/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json
+++ b/.metadata/.plugins/org.eclipse.buildship.core/gradle/versions.json
@@ -1,7 +1,7 @@
[ {
- "version" : "9.5.1-20260506020515+0000",
- "buildTime" : "20260506020515+0000",
- "commitId" : "b040c334940608c621fdbb778381ebdae66606d4",
+ "version" : "9.5.1-20260510022507+0000",
+ "buildTime" : "20260510022507+0000",
+ "commitId" : "fd78213f09782e62ca4957f9cfd3d90c6c3f1767",
"current" : false,
"snapshot" : true,
"nightly" : false,
@@ -10,15 +10,15 @@
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
- "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260506020515+0000-bin.zip",
- "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260506020515+0000-bin.zip.sha256",
- "checksum" : "ac52cb57303ce63a3131a7c079776ba70fc2b3e317dd311a27c092c96dc953f4",
- "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260506020515+0000-wrapper.jar.sha256",
+ "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260510022507+0000-bin.zip",
+ "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260510022507+0000-bin.zip.sha256",
+ "checksum" : "31ee63072850e69db0372d24655dbef7680aee3afaec2442d6395fc7ca672fd2",
+ "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.5.1-20260510022507+0000-wrapper.jar.sha256",
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
}, {
- "version" : "9.6.0-20260506005135+0000",
- "buildTime" : "20260506005135+0000",
- "commitId" : "44a190b8db5f6911020cba90f7a2642c02eba7bb",
+ "version" : "9.6.0-20260510003052+0000",
+ "buildTime" : "20260510003052+0000",
+ "commitId" : "62000451ad7b25de53fa89a155ef8ecb401621bb",
"current" : false,
"snapshot" : true,
"nightly" : true,
@@ -27,11 +27,28 @@
"rcFor" : "",
"milestoneFor" : "",
"broken" : false,
- "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260506005135+0000-bin.zip",
- "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260506005135+0000-bin.zip.sha256",
- "checksum" : "b746b4a02191ca19a49a8d492e371328b65d251f5aa463d9f68f96349d969546",
- "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260506005135+0000-wrapper.jar.sha256",
+ "downloadUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260510003052+0000-bin.zip",
+ "checksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260510003052+0000-bin.zip.sha256",
+ "checksum" : "7dca215df18a26f56705b82a860eed7dde169117d0f298d4ed0a296778a3e1e4",
+ "wrapperChecksumUrl" : "https://services.gradle.org/distributions-snapshots/gradle-9.6.0-20260510003052+0000-wrapper.jar.sha256",
"wrapperChecksum" : "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7"
+}, {
+ "version" : "8.14.5",
+ "buildTime" : "20260507110329+0000",
+ "commitId" : "62345becae08b13e793521816d585102fea66398",
+ "current" : false,
+ "snapshot" : false,
+ "nightly" : false,
+ "releaseNightly" : false,
+ "activeRc" : false,
+ "rcFor" : "",
+ "milestoneFor" : "",
+ "broken" : false,
+ "downloadUrl" : "https://services.gradle.org/distributions/gradle-8.14.5-bin.zip",
+ "checksumUrl" : "https://services.gradle.org/distributions/gradle-8.14.5-bin.zip.sha256",
+ "checksum" : "6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854",
+ "wrapperChecksumUrl" : "https://services.gradle.org/distributions/gradle-8.14.5-wrapper.jar.sha256",
+ "wrapperChecksum" : "7d3a4ac4de1c32b59bc6a4eb8ecb8e612ccd0cf1ae1e99f66902da64df296172"
}, {
"version" : "9.5.0",
"buildTime" : "20260428120530+0000",
diff --git a/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle b/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle
index 6153e78..b924492 100644
--- a/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle
+++ b/.metadata/.plugins/org.eclipse.buildship.core/init.d/eclipsePlugin.gradle
@@ -1,14 +1,14 @@
-/*******************************************************************************
- * Copyright (c) 2023 Gradle Inc. and others
- *
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-initscript {
- allprojects {
- apply plugin: "eclipse"
- }
+/*******************************************************************************
+ * Copyright (c) 2023 Gradle Inc. and others
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ******************************************************************************/
+initscript {
+ allprojects {
+ apply plugin: "eclipse"
+ }
}
\ No newline at end of file
diff --git a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game
index b7e6497..3156a95 100644
--- a/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game
+++ b/.metadata/.plugins/org.eclipse.buildship.core/project-preferences/blight-game
@@ -1,12 +1,12 @@
-#
-#Wed May 06 22:54:49 CEST 2026
-buildDir=build
-buildScriptPath=build.gradle
-classpath=\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n\t\n\t\t\n\t\n\n\n
-derivedResources=.gradle\:build
-gradleVersion=8.7
-hasAutoBuildTasks=false
-linkedResources=
-managedBuilders=
-managedNatures=
-subprojectPaths=
+#
+#Sun May 10 10:46:10 CEST 2026
+buildDir=build
+buildScriptPath=build.gradle
+classpath=\n\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\r\n\t\r\n\t\t\r\n\t\r\n\r\n\n
+derivedResources=.gradle;build
+gradleVersion=8.9
+hasAutoBuildTasks=false
+linkedResources=
+managedBuilders=
+managedNatures=
+subprojectPaths=
diff --git a/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml
index 4498ff9..9d6570f 100644
--- a/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml
+++ b/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml
@@ -1,13 +1,13 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/2/1073fe5c8a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/2/1073fe5c8a4900111396ab8a4c8e537b
deleted file mode 100644
index 3956c47..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/2/1073fe5c8a4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,12 +0,0 @@
-arguments=
-auto.sync=false
-build.scans.enabled=false
-connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
-eclipse.preferences.version=1
-gradle.user.home=
-java.home=
-jvm.arguments=
-offline.mode=false
-override.workspace.settings=false
-show.console.view=false
-show.executions.view=false
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/5/500711f38a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/5/500711f38a4900111396ab8a4c8e537b
deleted file mode 100644
index 8377c9d..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/5/500711f38a4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.blight.game;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.system.AppSettings;
-import de.blight.game.scene.WorldScene;
-
-public class BlightApp extends SimpleApplication {
-
- public static void main(String[] args) {
- BlightApp app = new BlightApp();
-
- AppSettings settings = new AppSettings(true);
- settings.setTitle("Blight");
- settings.setResolution(1280, 720);
- settings.setFrameRate(60);
- settings.setVSync(true);
- settings.setSamples(4);
-
- app.setSettings(settings);
- app.setShowSettings(false);
- app.start();
- }
-
- @Override
- public void simpleInitApp() {
- // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst
- flyCam.setEnabled(false);
-
- stateManager.attach(new WorldScene());
- }
-
- @Override
- public void simpleUpdate(float tpf) {
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/7/90d9d2fb8a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/7/90d9d2fb8a4900111396ab8a4c8e537b
deleted file mode 100644
index 8b341c0..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/7/90d9d2fb8a4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.blight.game;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.system.AppSettings;
-import de.blight.game.scene.WorldScene;
-
-public class BlightApp extends SimpleApplication {
-
- public static void main(String[] args) {
- BlightApp app = new BlightApp();
-
- AppSettings settings = new AppSettings(true);
- settings.setTitle("Blight");
- settings.setResolution(1280, 720);
- settings.setVSync(true);
- settings.setSamples(4);
-
- app.setSettings(settings);
- app.setShowSettings(false);
- app.start();
- }
-
- @Override
- public void simpleInitApp() {
- // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst
- flyCam.setEnabled(false);
-
- stateManager.attach(new WorldScene());
- }
-
- @Override
- public void simpleUpdate(float tpf) {
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/7e/2089c33c8d4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/7e/2089c33c8d4900111396ab8a4c8e537b
deleted file mode 100644
index 5859c5b..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/7e/2089c33c8d4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,307 +0,0 @@
-package de.blight.game.config;
-
-import com.jme3.app.Application;
-import com.jme3.app.SimpleApplication;
-import com.jme3.app.state.BaseAppState;
-import com.jme3.font.BitmapFont;
-import com.jme3.font.BitmapText;
-import com.jme3.input.KeyInput;
-import com.jme3.input.MouseInput;
-import com.jme3.input.RawInputListener;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.MouseButtonTrigger;
-import com.jme3.input.event.*;
-import com.jme3.input.event.KeyInputEvent;
-import com.jme3.input.event.MouseButtonEvent;
-import com.jme3.input.event.MouseMotionEvent;
-import com.jme3.input.event.JoyAxisEvent;
-import com.jme3.input.event.JoyButtonEvent;
-import com.jme3.input.event.TouchEvent;
-import com.jme3.material.Material;
-import com.jme3.material.RenderState;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Vector2f;
-import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Node;
-import com.jme3.scene.shape.Quad;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Overlay-AppState der die Tastenbelegungs-Maske anzeigt.
- *
- * ESC → Schließen (ohne Speichern)
- * Klick auf Row → wartet auf neue Taste
- * ESC während Warten → bricht nur die Zuweisung ab
- * Speichern → schreibt JSON, ruft onSave-Callback
- */
-public class ConfigScreen extends BaseAppState implements RawInputListener {
-
- // Farben
- private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
- private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
- private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
- private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f);
- private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
- private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
- private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
- private static final ColorRGBA COL_TEXT = ColorRGBA.White;
- private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
-
- // -----------------------------------------------------------------------
-
- private SimpleApplication app;
- private Node guiNode;
- private BitmapFont font;
-
- private KeyBindings liveBindings; // geteilt mit der ganzen App
- private KeyBindings editCopy; // wird beim Öffnen geklont
-
- private Runnable onSave; // Callback → PlayerInputControl.reloadBindings
-
- private Node panel;
- private List rows = new ArrayList<>();
- private int waitingRow = -1; // -1 = keine Zuweisung aktiv
-
- // UI-Elemente für Buttons (Bounds in Screen-Koordinaten)
- private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
- private float cancelBtnX, cancelBtnY;
-
- // -----------------------------------------------------------------------
-
- private static class Row {
- String field;
- String label;
- BitmapText keyText;
- Geometry bg;
- float x, y, w, h; // Button-Bounds
- }
-
- // -----------------------------------------------------------------------
-
- public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
- this.liveBindings = liveBindings;
- this.onSave = onSave;
- }
-
- public boolean isWaiting() { return waitingRow >= 0; }
-
- // -----------------------------------------------------------------------
- // Lifecycle
- // -----------------------------------------------------------------------
-
- @Override
- protected void initialize(Application app) {
- this.app = (SimpleApplication) app;
- this.guiNode = this.app.getGuiNode();
- this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
- }
-
- @Override
- protected void onEnable() {
- editCopy = liveBindings.copy();
- waitingRow = -1;
- buildUI();
- app.getInputManager().setCursorVisible(true);
- app.getInputManager().addRawInputListener(this);
- app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
- app.getInputManager().addListener(clickListener, "_CfgClick");
- }
-
- @Override
- protected void onDisable() {
- if (panel != null) { guiNode.detachChild(panel); panel = null; }
- rows.clear();
- waitingRow = -1;
- app.getInputManager().removeRawInputListener(this);
- app.getInputManager().deleteMapping("_CfgClick");
- app.getInputManager().setCursorVisible(false);
- }
-
- @Override protected void cleanup(Application app) {}
-
- // -----------------------------------------------------------------------
- // UI aufbauen
- // -----------------------------------------------------------------------
-
- private void buildUI() {
- float sw = app.getCamera().getWidth();
- float sh = app.getCamera().getHeight();
-
- panel = new Node("cfg-panel");
-
- // Halbdurchsichtiger Overlay über dem Spiel
- addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
-
- float pw = 720, ph = 440;
- float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
- addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
-
- // Titel
- BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT);
- centerText(title, px, py + ph - 40, pw);
- panel.attachChild(title);
-
- BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
- centerText(hint, px, py + ph - 70, pw);
- panel.attachChild(hint);
-
- // Reihen
- float rowX = px + 30;
- float keyX = px + pw - 220;
- float rowW = 180;
- float rowH = 36;
- float startY = py + ph - 110;
- float stepY = 48;
-
- for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
- String[] entry = KeyBindings.ENTRIES[i];
- float ry = startY - i * stepY;
-
- BitmapText lbl = text(entry[1], 16, COL_TEXT);
- lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
- panel.attachChild(lbl);
-
- Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
-
- BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
- kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
- panel.attachChild(kt);
-
- Row row = new Row();
- row.field = entry[0];
- row.label = entry[1];
- row.keyText = kt;
- row.bg = bg;
- row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH;
- rows.add(row);
- }
-
- // Buttons
- float btnW = 160, btnH = 42;
- float btnY = py + 25;
- saveBtnX = px + pw / 2f - btnW - 15;
- saveBtnY = btnY;
- saveBtnW = btnW;
- saveBtnH = btnH;
- cancelBtnX = px + pw / 2f + 15;
- cancelBtnY = btnY;
-
- addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0);
- BitmapText saveLabel = text("Speichern", 16, COL_TEXT);
- centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
- panel.attachChild(saveLabel);
-
- addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0);
- BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT);
- centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
- panel.attachChild(cancelLabel);
-
- guiNode.attachChild(panel);
- }
-
- // -----------------------------------------------------------------------
- // Mausklick
- // -----------------------------------------------------------------------
-
- private final ActionListener clickListener = (name, isPressed, tpf) -> {
- if (!isPressed) return;
- Vector2f cursor = app.getInputManager().getCursorPosition();
-
- // Reihen prüfen
- for (int i = 0; i < rows.size(); i++) {
- Row r = rows.get(i);
- if (hits(cursor, r.x, r.y, r.w, r.h)) {
- waitingRow = i;
- r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
- r.keyText.setText("...");
- return;
- }
- }
-
- // Speichern
- if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
- liveBindings.copyFrom(editCopy);
- KeyBindingStore.save(liveBindings);
- if (onSave != null) onSave.run();
- setEnabled(false);
- return;
- }
-
- // Abbrechen
- if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
- setEnabled(false);
- }
- };
-
- // -----------------------------------------------------------------------
- // Tastendruck beim Warten auf Zuweisung (RawInputListener)
- // -----------------------------------------------------------------------
-
- @Override
- public void onKeyEvent(KeyInputEvent evt) {
- if (!evt.isPressed() || waitingRow < 0) return;
-
- if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) {
- // Zuweisung abbrechen, Maske offen lassen
- resetRowColor(waitingRow);
- waitingRow = -1;
- return;
- }
-
- Row r = rows.get(waitingRow);
- editCopy.set(r.field, evt.getKeyCode());
- r.keyText.setText(KeyNames.of(evt.getKeyCode()));
- resetRowColor(waitingRow);
- waitingRow = -1;
- }
-
- private void resetRowColor(int idx) {
- rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
- }
-
- // RawInputListener-Pflichtmethoden
- @Override public void beginInput() {}
- @Override public void endInput() {}
- @Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
- @Override public void onMouseButtonEvent(MouseButtonEvent evt) {}
- @Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
- @Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
- @Override public void onTouchEvent(TouchEvent evt) {}
-
- // -----------------------------------------------------------------------
- // Hilfsmethoden
- // -----------------------------------------------------------------------
-
- private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
- Geometry geo = new Geometry("q", new Quad(w, h));
- Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", color.clone());
- if (color.a < 1f) {
- mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- geo.setQueueBucket(RenderQueue.Bucket.Transparent);
- }
- geo.setMaterial(mat);
- geo.setLocalTranslation(x, y, z);
- parent.attachChild(geo);
- return geo;
- }
-
- private BitmapText text(String content, int size, ColorRGBA color) {
- BitmapText t = new BitmapText(font, false);
- t.setSize(size);
- t.setColor(color);
- t.setText(content);
- return t;
- }
-
- private void centerText(BitmapText t, float x, float y, float width) {
- t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
- }
-
- private boolean hits(Vector2f p, float x, float y, float w, float h) {
- return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/98/a08ecd828a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/98/a08ecd828a4900111396ab8a4c8e537b
deleted file mode 100644
index a64207a..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/98/a08ecd828a4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,90 +0,0 @@
-package de.blight.game.control;
-
-import com.jme3.input.InputManager;
-import com.jme3.input.KeyInput;
-import com.jme3.input.MouseInput;
-import com.jme3.input.controls.*;
-import com.jme3.math.*;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.Spatial;
-
-/**
- * Bewegt einen Charakter relativ zur Kamera-Horizontal-Richtung
- * und dreht ihn in Bewegungsrichtung.
- */
-public class CharacterControl {
-
- private static final float MOVE_SPEED = 8f;
- private static final float ROTATE_SPEED = 5f;
-
- private final InputManager inputManager;
- private final Camera cam;
-
- private Spatial character;
-
- // Eingabezustand
- private boolean forward, backward, left, right;
-
- public CharacterControl(InputManager inputManager, Camera cam) {
- this.inputManager = inputManager;
- this.cam = cam;
- registerMappings();
- }
-
- public void setCharacter(Spatial character) {
- this.character = character;
- }
-
- // -----------------------------------------------------------------------
- // Eingabe-Registrierung
- // -----------------------------------------------------------------------
-
- private void registerMappings() {
- inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W), new KeyTrigger(KeyInput.KEY_UP));
- inputManager.addMapping("Backward", new KeyTrigger(KeyInput.KEY_S), new KeyTrigger(KeyInput.KEY_DOWN));
- inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A), new KeyTrigger(KeyInput.KEY_LEFT));
- inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D), new KeyTrigger(KeyInput.KEY_RIGHT));
-
- ActionListener listener = (name, isPressed, tpf) -> {
- switch (name) {
- case "Forward" -> forward = isPressed;
- case "Backward" -> backward = isPressed;
- case "Left" -> left = isPressed;
- case "Right" -> right = isPressed;
- }
- };
-
- inputManager.addListener(listener, "Forward", "Backward", "Left", "Right");
- }
-
- // -----------------------------------------------------------------------
- // Update pro Frame
- // -----------------------------------------------------------------------
-
- public void update(float tpf) {
- if (character == null) return;
-
- // Kamerahorizontale Vorwärtsrichtung (Y ignorieren)
- Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
- Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
-
- Vector3f moveDir = Vector3f.ZERO.clone();
- if (forward) moveDir.addLocal(camDir);
- if (backward) moveDir.subtractLocal(camDir);
- if (left) moveDir.addLocal(camLeft);
- if (right) moveDir.subtractLocal(camLeft);
-
- if (moveDir.lengthSquared() > 0.001f) {
- moveDir.normalizeLocal();
- character.move(moveDir.mult(MOVE_SPEED * tpf));
-
- // Charakter zur Bewegungsrichtung drehen
- Quaternion targetRot = new Quaternion();
- targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
-
- Quaternion current = character.getLocalRotation();
- current.slerp(targetRot, ROTATE_SPEED * tpf);
- character.setLocalRotation(current);
- }
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/b9/f019f32b8b4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/b9/f019f32b8b4900111396ab8a4c8e537b
deleted file mode 100644
index 6c0f1c7..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/b9/f019f32b8b4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.blight.game;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.system.AppSettings;
-import de.blight.game.scene.WorldScene;
-
-public class BlightApp extends SimpleApplication {
-
- public static void main(String[] args) {
- BlightApp app = new BlightApp();
-
- AppSettings settings = new AppSettings(true);
- settings.setTitle("Blight");
- settings.setResolution(1280, 720);
- settings.setSamples(4);
-
- app.setSettings(settings);
- app.setShowSettings(true);
- app.start();
- }
-
- @Override
- public void simpleInitApp() {
- // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst
- flyCam.setEnabled(false);
-
- stateManager.attach(new WorldScene());
- }
-
- @Override
- public void simpleUpdate(float tpf) {
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/dc/10c4d9948a4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/dc/10c4d9948a4900111396ab8a4c8e537b
deleted file mode 100644
index 7877a1e..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/dc/10c4d9948a4900111396ab8a4c8e537b
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'blight'
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/ea/408ace308b4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/ea/408ace308b4900111396ab8a4c8e537b
deleted file mode 100644
index 738b45a..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/ea/408ace308b4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.blight.game;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.system.AppSettings;
-import de.blight.game.scene.WorldScene;
-
-public class BlightApp extends SimpleApplication {
-
- public static void main(String[] args) {
- BlightApp app = new BlightApp();
-
- app.start();
- }
-
- @Override
- public void simpleInitApp() {
- // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst
- flyCam.setEnabled(false);
-
- stateManager.attach(new WorldScene());
- }
-
- @Override
- public void simpleUpdate(float tpf) {
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.history/f0/b0f574138b4900111396ab8a4c8e537b b/.metadata/.plugins/org.eclipse.core.resources/.history/f0/b0f574138b4900111396ab8a4c8e537b
deleted file mode 100644
index d3af7c8..0000000
--- a/.metadata/.plugins/org.eclipse.core.resources/.history/f0/b0f574138b4900111396ab8a4c8e537b
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.blight.game;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.system.AppSettings;
-import de.blight.game.scene.WorldScene;
-
-public class BlightApp extends SimpleApplication {
-
- public static void main(String[] args) {
- BlightApp app = new BlightApp();
-
- AppSettings settings = new AppSettings(true);
- settings.setTitle("Blight");
- settings.setResolution(1280, 720);
- settings.setSamples(4);
-
- app.setSettings(settings);
- app.setShowSettings(false);
- app.start();
- }
-
- @Override
- public void simpleInitApp() {
- // Standard-FlyCamera deaktivieren — wir steuern die Kamera selbst
- flyCam.setEnabled(false);
-
- stateManager.attach(new WorldScene());
- }
-
- @Override
- public void simpleUpdate(float tpf) {
- }
-}
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location
index 086253f..b2c6373 100644
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location and b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.location differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.markers.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.markers.snap
deleted file mode 100644
index 91d6c54..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.markers.snap and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.syncinfo.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.syncinfo.snap
deleted file mode 100644
index 91d6c54..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/.org.eclipse.egit.core.cmp/.syncinfo.snap and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index
index f58f0eb..b80d5a7 100644
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index and b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/af/history.index differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/5d/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/5d/history.index
deleted file mode 100644
index 6a07d65..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/5d/history.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/be/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/be/history.index
deleted file mode 100644
index 45354d5..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/be/history.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/history.index
deleted file mode 100644
index 9489504..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/e4/b9/22/81/c/f2/history.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/history.index b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/history.index
deleted file mode 100644
index 16492ee..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.indexes/history.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location
index 2e68d47..69d80cd 100644
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location and b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.location differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers
index 78b801f..b8b6ac1 100644
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers and b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers.snap
deleted file mode 100644
index 74ee311..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.markers.snap and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.syncinfo.snap b/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.syncinfo.snap
deleted file mode 100644
index 91d6c54..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.projects/blight-game/.syncinfo.snap and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index
index 12acdc0..227e6f2 100644
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index and b/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.index differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/.markers.snap b/.metadata/.plugins/org.eclipse.core.resources/.root/.markers.snap
deleted file mode 100644
index bdb00bf..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.root/.markers.snap and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree b/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree
deleted file mode 100644
index 0fb4d8a..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.root/2.tree and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources
index 22417f5..155c4ee 100644
Binary files a/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources and b/.metadata/.plugins/org.eclipse.core.resources/.safetable/org.eclipse.core.resources differ
diff --git a/.metadata/.plugins/org.eclipse.core.resources/2.snap b/.metadata/.plugins/org.eclipse.core.resources/2.snap
deleted file mode 100644
index 96076fa..0000000
Binary files a/.metadata/.plugins/org.eclipse.core.resources/2.snap and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml b/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml
index c83f1a9..3a94050 100644
--- a/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml
+++ b/.metadata/.plugins/org.eclipse.debug.ui/launchConfigurationHistory.xml
@@ -1,30 +1,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi
index 1c29e6a..19b0fa1 100644
--- a/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi
+++ b/.metadata/.plugins/org.eclipse.e4.workbench/workbench.xmi
@@ -1,2764 +1,3342 @@
-
-
-
- activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration
-
-
-
-
-
-
-
- topLevel
- shellMaximized
-
-
-
-
- persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation
- persp.actionSet:org.eclipse.ui.cheatsheets.actionSet
- persp.actionSet:org.eclipse.search.searchActionSet
- persp.actionSet:org.eclipse.text.quicksearch.actionSet
- persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation
- persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation
- persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo
- persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet
- persp.actionSet:org.eclipse.ui.actionSet.keyBindings
- persp.actionSet:org.eclipse.ui.actionSet.openFiles
- persp.actionSet:org.springsource.ide.eclipse.commons.launch.actionSet
- persp.actionSet:org.eclipse.debug.ui.launchActionSet
- persp.actionSet:org.eclipse.jdt.ui.JavaActionSet
- persp.actionSet:org.eclipse.jdt.ui.JavaElementCreationActionSet
- persp.actionSet:org.eclipse.ui.NavigateActionSet
- persp.viewSC:org.eclipse.jdt.ui.PackageExplorer
- persp.viewSC:org.eclipse.jdt.ui.TypeHierarchy
- persp.viewSC:org.eclipse.jdt.ui.SourceView
- persp.viewSC:org.eclipse.jdt.ui.JavadocView
- persp.viewSC:org.eclipse.search.ui.views.SearchView
- persp.viewSC:org.eclipse.ui.console.ConsoleView
- persp.viewSC:org.eclipse.ui.views.ContentOutline
- persp.viewSC:org.eclipse.ui.views.ProblemView
- persp.viewSC:org.eclipse.ui.views.TaskList
- persp.viewSC:org.eclipse.ui.views.ProgressView
- persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer
- persp.viewSC:org.eclipse.ui.texteditor.TemplatesView
- persp.viewSC:org.eclipse.pde.runtime.LogView
- persp.newWizSC:org.eclipse.jdt.ui.wizards.JavaProjectWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewPackageCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewClassCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewInterfaceCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewEnumCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewRecordCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewAnnotationCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSourceFolderCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSnippetFileCreationWizard
- persp.newWizSC:org.eclipse.jdt.ui.wizards.NewJavaWorkingSetWizard
- persp.newWizSC:org.eclipse.ui.wizards.new.folder
- persp.newWizSC:org.eclipse.ui.wizards.new.file
- persp.newWizSC:org.eclipse.ui.editors.wizards.UntitledTextFileWizard
- persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective
- persp.perspSC:org.eclipse.debug.ui.DebugPerspective
- persp.showIn:org.eclipse.jdt.ui.PackageExplorer
- persp.showIn:org.eclipse.team.ui.GenericHistoryView
- persp.showIn:org.eclipse.ui.navigator.ProjectExplorer
- persp.viewSC:org.eclipse.mylyn.tasks.ui.views.tasks
- persp.newWizSC:org.eclipse.mylyn.tasks.ui.wizards.new.repository.task
- persp.viewSC:org.eclipse.terminal.view.ui.TerminalsView
- persp.showIn:org.eclipse.terminal.view.ui.TerminalsView
- persp.actionSet:org.eclipse.debug.ui.breakpointActionSet
- persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet
- persp.newWizSC:org.eclipse.m2e.core.wizards.Maven2ProjectWizard
- persp.newWizSC:org.springsource.ide.eclipse.commons.gettingstarted.wizard.boot.NewSpringBootWizard
- persp.newWizSC:org.springsource.ide.eclipse.gettingstarted.wizards.import.generic.newalias
- persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet
- persp.showIn:org.eclipse.eclemma.ui.CoverageView
- persp.viewSC:org.eclipse.jdt.bcoview.views.BytecodeOutlineView
- persp.showIn:org.eclipse.egit.ui.RepositoriesView
- persp.newWizSC:org.eclipse.jdt.junit.wizards.NewTestCaseCreationWizard
- persp.actionSet:org.eclipse.jdt.junit.JUnitActionSet
- persp.viewSC:org.eclipse.ant.ui.views.AntView
- persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.jdt.ui/$nl$/icons/full/onboarding_jperspective.svg
- persp.editorOnboardingText:Open a file or drop files here to open them.
- persp.editorOnboardingCommand:Find Actions$$$Ctrl+3
- persp.editorOnboardingCommand:Show Key Assist$$$Shift+Ctrl+L
- persp.editorOnboardingCommand:New$$$Ctrl+N
- persp.editorOnboardingCommand:Open Type$$$Shift+Ctrl+T
-
-
-
-
- org.eclipse.e4.primaryNavigationStack
-
- View
- categoryTag:Java
-
-
- View
- categoryTag:Java
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Spring
-
-
-
-
-
- View
- categoryTag:Git
-
-
-
-
-
-
-
- org.eclipse.e4.secondaryNavigationStack
-
- View
- categoryTag:General
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:Mylyn
-
-
- View
- categoryTag:Java
-
-
- View
- categoryTag:Ant
-
-
-
-
- org.eclipse.e4.secondaryDataStack
- Oomph
- Gradle
-
- View
- categoryTag:General
-
-
- View
- categoryTag:Java
-
-
- View
- categoryTag:Java
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:Terminal
-
-
- View
- categoryTag:Gradle
-
-
- View
- categoryTag:Gradle
-
-
- View
- categoryTag:Oomph
- NoRestore
-
-
- View
- categoryTag:Oomph
- NoRestore
-
-
-
-
-
-
-
-
- View
- categoryTag:Help
-
-
- View
- categoryTag:General
-
-
- View
- categoryTag:Help
-
-
-
-
-
-
- View
- categoryTag:Help
-
-
-
-
-
- View
- categoryTag:General
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
- View
- categoryTag:Help
-
-
-
- EditorStack
- org.eclipse.e4.primaryDataStack
- active
- noFocus
-
-
- Editor
- removeOnHide
- org.eclipse.buildship.ui.gradlebuildscripteditor
-
-
-
- Editor
- removeOnHide
- org.eclipse.jdt.ui.CompilationUnitEditor
-
-
-
- Editor
- removeOnHide
- org.eclipse.jdt.ui.CompilationUnitEditor
- active
-
-
-
-
-
-
-
- View
- categoryTag:Java
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:General
-
-
-
-
-
- View
- categoryTag:General
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:General
-
-
-
-
-
- View
- categoryTag:General
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
-
- View
- categoryTag:General
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Terminal
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Git
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
-
- View
- categoryTag:Spring
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
- View
- categoryTag:Ant
-
-
-
-
-
- View
- categoryTag:Gradle
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
-
- View
- categoryTag:Gradle
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
-
- View
- categoryTag:Oomph
- NoRestore
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
-
-
- View
- categoryTag:Oomph
- NoRestore
-
- ViewMenu
- menuContribution:menu
-
-
-
-
-
- toolbarSeparator
-
-
-
- Draggable
-
-
-
- toolbarSeparator
-
-
-
- Draggable
-
-
-
-
- toolbarSeparator
-
-
-
- Draggable
-
-
- Draggable
-
-
- Draggable
-
-
- Draggable
-
-
- toolbarSeparator
-
-
-
- Draggable
-
-
-
- Draggable
-
-
- toolbarSeparator
-
-
-
- toolbarSeparator
-
-
-
- Draggable
-
-
- stretch
- SHOW_RESTORE_MENU
-
-
- Draggable
- HIDEABLE
- SHOW_RESTORE_MENU
-
-
-
-
- stretch
-
-
- Draggable
-
-
- Draggable
-
-
-
-
- TrimStack
- Draggable
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- platform:gtk
-
-
-
-
-
- platform:gtk
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- platform:gtk
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Editor
- removeOnHide
-
-
-
-
- View
- categoryTag:Ant
-
-
-
-
- View
- categoryTag:Gradle
-
-
-
-
- View
- categoryTag:Gradle
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Debug
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Git
-
-
-
-
- View
- categoryTag:Git
-
-
-
-
- View
- categoryTag:Git
-
-
-
-
- View
- categoryTag:Git
- NoRestore
-
-
-
-
- View
- categoryTag:Git
-
-
-
-
- View
- categoryTag:Help
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Debug
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Java Browsing
-
-
-
-
- View
- categoryTag:Java Browsing
-
-
-
-
- View
- categoryTag:Java Browsing
-
-
-
-
- View
- categoryTag:Java Browsing
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Java
-
-
-
-
- View
- categoryTag:Docker
-
-
-
-
- View
- categoryTag:Docker
-
-
-
-
- View
- categoryTag:Docker
-
-
-
-
- View
- categoryTag:Docker
-
-
-
-
- View
- categoryTag:Language Servers
-
-
-
-
- View
- categoryTag:Language Servers
-
-
-
-
- View
- categoryTag:Language Servers
-
-
-
-
- View
- categoryTag:Maven
-
-
-
-
- View
- categoryTag:Maven
-
-
-
-
- View
- categoryTag:Maven
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Mylyn
-
-
-
-
- View
- categoryTag:Oomph
-
-
-
-
- View
- categoryTag:Oomph
- NoRestore
-
-
-
-
- View
- categoryTag:Plug-in Development
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:Version Control (Team)
-
-
-
-
- View
- categoryTag:Version Control (Team)
-
-
-
-
- View
- categoryTag:Terminal
-
-
- View
- categoryTag:Help
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:Help
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:General
-
-
-
-
- View
- categoryTag:Spring
-
-
-
-
- View
- categoryTag:Spring
-
-
-
-
- View
- categoryTag:Spring
-
-
-
- glue
- move_after:PerspectiveSpacer
- SHOW_RESTORE_MENU
-
-
- move_after:Spacer Glue
- HIDEABLE
- SHOW_RESTORE_MENU
-
-
- glue
- move_after:SearchField
- SHOW_RESTORE_MENU
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ activeSchemeId:org.eclipse.ui.defaultAcceleratorConfiguration
+
+
+
+
+
+
+
+ topLevel
+ shellMinimized
+
+
+
+
+ persp.actionSet:org.eclipse.mylyn.tasks.ui.navigation
+ persp.actionSet:org.eclipse.ui.cheatsheets.actionSet
+ persp.actionSet:org.eclipse.search.searchActionSet
+ persp.actionSet:org.eclipse.text.quicksearch.actionSet
+ persp.actionSet:org.eclipse.ui.edit.text.actionSet.annotationNavigation
+ persp.actionSet:org.eclipse.ui.edit.text.actionSet.navigation
+ persp.actionSet:org.eclipse.ui.edit.text.actionSet.convertLineDelimitersTo
+ persp.actionSet:org.eclipse.ui.externaltools.ExternalToolsSet
+ persp.actionSet:org.eclipse.ui.actionSet.keyBindings
+ persp.actionSet:org.eclipse.ui.actionSet.openFiles
+ persp.actionSet:org.springsource.ide.eclipse.commons.launch.actionSet
+ persp.actionSet:org.eclipse.debug.ui.launchActionSet
+ persp.actionSet:org.eclipse.jdt.ui.JavaActionSet
+ persp.actionSet:org.eclipse.jdt.ui.JavaElementCreationActionSet
+ persp.actionSet:org.eclipse.ui.NavigateActionSet
+ persp.viewSC:org.eclipse.jdt.ui.PackageExplorer
+ persp.viewSC:org.eclipse.jdt.ui.TypeHierarchy
+ persp.viewSC:org.eclipse.jdt.ui.SourceView
+ persp.viewSC:org.eclipse.jdt.ui.JavadocView
+ persp.viewSC:org.eclipse.search.ui.views.SearchView
+ persp.viewSC:org.eclipse.ui.console.ConsoleView
+ persp.viewSC:org.eclipse.ui.views.ContentOutline
+ persp.viewSC:org.eclipse.ui.views.ProblemView
+ persp.viewSC:org.eclipse.ui.views.TaskList
+ persp.viewSC:org.eclipse.ui.views.ProgressView
+ persp.viewSC:org.eclipse.ui.navigator.ProjectExplorer
+ persp.viewSC:org.eclipse.ui.texteditor.TemplatesView
+ persp.viewSC:org.eclipse.pde.runtime.LogView
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.JavaProjectWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewPackageCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewClassCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewInterfaceCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewEnumCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewRecordCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewAnnotationCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSourceFolderCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewSnippetFileCreationWizard
+ persp.newWizSC:org.eclipse.jdt.ui.wizards.NewJavaWorkingSetWizard
+ persp.newWizSC:org.eclipse.ui.wizards.new.folder
+ persp.newWizSC:org.eclipse.ui.wizards.new.file
+ persp.newWizSC:org.eclipse.ui.editors.wizards.UntitledTextFileWizard
+ persp.perspSC:org.eclipse.jdt.ui.JavaBrowsingPerspective
+ persp.perspSC:org.eclipse.debug.ui.DebugPerspective
+ persp.showIn:org.eclipse.jdt.ui.PackageExplorer
+ persp.showIn:org.eclipse.team.ui.GenericHistoryView
+ persp.showIn:org.eclipse.ui.navigator.ProjectExplorer
+ persp.viewSC:org.eclipse.mylyn.tasks.ui.views.tasks
+ persp.newWizSC:org.eclipse.mylyn.tasks.ui.wizards.new.repository.task
+ persp.viewSC:org.eclipse.terminal.view.ui.TerminalsView
+ persp.showIn:org.eclipse.terminal.view.ui.TerminalsView
+ persp.actionSet:org.eclipse.debug.ui.breakpointActionSet
+ persp.actionSet:org.eclipse.jdt.debug.ui.JDTDebugActionSet
+ persp.newWizSC:org.eclipse.m2e.core.wizards.Maven2ProjectWizard
+ persp.newWizSC:org.springsource.ide.eclipse.commons.gettingstarted.wizard.boot.NewSpringBootWizard
+ persp.newWizSC:org.springsource.ide.eclipse.gettingstarted.wizards.import.generic.newalias
+ persp.actionSet:org.eclipse.eclemma.ui.CoverageActionSet
+ persp.showIn:org.eclipse.eclemma.ui.CoverageView
+ persp.viewSC:org.eclipse.jdt.bcoview.views.BytecodeOutlineView
+ persp.showIn:org.eclipse.egit.ui.RepositoriesView
+ persp.newWizSC:org.eclipse.jdt.junit.wizards.NewTestCaseCreationWizard
+ persp.actionSet:org.eclipse.jdt.junit.JUnitActionSet
+ persp.viewSC:org.eclipse.ant.ui.views.AntView
+ persp.editorOnboardingImageUri:platform:/plugin/org.eclipse.jdt.ui/$nl$/icons/full/onboarding_jperspective.svg
+ persp.editorOnboardingText:Open a file or drop files here to open them.
+ persp.editorOnboardingCommand:Find Actions$$$Ctrl+3
+ persp.editorOnboardingCommand:Show Key Assist$$$Ctrl+Shift+L
+ persp.editorOnboardingCommand:New$$$Ctrl+N
+ persp.editorOnboardingCommand:Open Type$$$Ctrl+Shift+T
+
+
+
+
+ org.eclipse.e4.primaryNavigationStack
+ active
+ noFocus
+
+ View
+ categoryTag:Java
+
+
+ View
+ categoryTag:Java
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Spring
+
+
+
+
+
+ View
+ categoryTag:Git
+
+
+
+
+
+
+
+ org.eclipse.e4.secondaryNavigationStack
+ Minimized
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:Mylyn
+
+
+ View
+ categoryTag:Java
+
+
+ View
+ categoryTag:Ant
+
+
+
+
+ org.eclipse.e4.secondaryDataStack
+ Oomph
+ Gradle
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:Java
+
+
+ View
+ categoryTag:Java
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:Terminal
+
+
+ View
+ categoryTag:Gradle
+
+
+ View
+ categoryTag:Gradle
+
+
+
+
+
+
+
+
+ View
+ categoryTag:Help
+
+
+ View
+ categoryTag:General
+
+
+ View
+ categoryTag:Help
+
+
+
+
+
+
+ View
+ categoryTag:Help
+
+
+
+
+
+ View
+ categoryTag:General
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+ View
+ categoryTag:Help
+
+
+
+ EditorStack
+ org.eclipse.e4.primaryDataStack
+
+
+ Editor
+ removeOnHide
+ org.eclipse.buildship.ui.gradlebuildscripteditor
+
+
+
+ Editor
+ removeOnHide
+ org.eclipse.buildship.ui.gradlebuildscripteditor
+
+
+
+
+
+
+
+ View
+ categoryTag:Java
+ active
+ activeOnClose
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+
+ View
+ categoryTag:General
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+
+ View
+ categoryTag:General
+ highlighted
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+
+ View
+ categoryTag:General
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+
+ View
+ categoryTag:General
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Terminal
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Git
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+
+ View
+ categoryTag:Spring
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+ View
+ categoryTag:Ant
+
+
+
+
+
+ View
+ categoryTag:Gradle
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+
+
+ View
+ categoryTag:Gradle
+
+ ViewMenu
+ menuContribution:menu
+
+
+
+
+
+ toolbarSeparator
+
+
+
+ Draggable
+
+
+
+ toolbarSeparator
+
+
+
+ Draggable
+
+
+
+
+ toolbarSeparator
+
+
+
+ Draggable
+
+
+ Draggable
+
+
+ Draggable
+
+
+ Draggable
+
+
+ toolbarSeparator
+
+
+
+ Draggable
+
+
+
+ toolbarSeparator
+
+
+
+ toolbarSeparator
+
+
+
+ Draggable
+
+
+ stretch
+ SHOW_RESTORE_MENU
+
+
+ Draggable
+ HIDEABLE
+ SHOW_RESTORE_MENU
+
+
+
+
+ stretch
+
+
+ Draggable
+
+
+ Draggable
+
+
+
+
+ TrimStack
+ Draggable
+
+
+ TrimStack
+ Draggable
+
+
+ TrimStack
+ Draggable
+
+
+
+
+ TrimStack
+ Draggable
+
+
+ TrimStack
+ Draggable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ platform:win32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ locale:de
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ platform:win32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Editor
+ removeOnHide
+
+
+
+
+ View
+ categoryTag:Ant
+
+
+
+
+ View
+ categoryTag:Gradle
+
+
+
+
+ View
+ categoryTag:Gradle
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Git
+
+
+
+
+ View
+ categoryTag:Git
+
+
+
+
+ View
+ categoryTag:Git
+
+
+
+
+ View
+ categoryTag:Git
+ NoRestore
+
+
+
+
+ View
+ categoryTag:Git
+
+
+
+
+ View
+ categoryTag:Help
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Java Browsing
+
+
+
+
+ View
+ categoryTag:Java Browsing
+
+
+
+
+ View
+ categoryTag:Java Browsing
+
+
+
+
+ View
+ categoryTag:Java Browsing
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Java
+
+
+
+
+ View
+ categoryTag:Docker
+
+
+
+
+ View
+ categoryTag:Docker
+
+
+
+
+ View
+ categoryTag:Docker
+
+
+
+
+ View
+ categoryTag:Docker
+
+
+
+
+ View
+ categoryTag:Language Servers
+
+
+
+
+ View
+ categoryTag:Language Servers
+
+
+
+
+ View
+ categoryTag:Language Servers
+
+
+
+
+ View
+ categoryTag:Maven
+
+
+
+
+ View
+ categoryTag:Maven
+
+
+
+
+ View
+ categoryTag:Maven
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Mylyn
+
+
+
+
+ View
+ categoryTag:Oomph
+
+
+
+
+ View
+ categoryTag:Oomph
+ NoRestore
+
+
+
+
+ View
+ categoryTag:Plug-in Development
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:Version Control (Team)
+
+
+
+
+ View
+ categoryTag:Version Control (Team)
+
+
+
+
+ View
+ categoryTag:Terminal
+
+
+ View
+ categoryTag:Help
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:Help
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:Spring
+
+
+
+
+ View
+ categoryTag:Spring
+
+
+
+
+ View
+ categoryTag:Spring
+
+
+
+
+ View
+ categoryTag:Data Management
+
+
+
+
+ View
+ categoryTag:Data Management
+
+
+
+
+ View
+ categoryTag:Data Management
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:JPA
+
+
+
+
+ View
+ categoryTag:JPA
+
+
+
+
+ View
+ categoryTag:JavaServer Faces
+
+
+
+
+ View
+ categoryTag:JavaServer Faces
+
+
+
+
+ View
+ categoryTag:Web Services
+
+
+
+
+ View
+ categoryTag:API Tools
+
+
+
+
+ View
+ categoryTag:OSGi
+
+
+
+
+ View
+ categoryTag:OSGi
+
+
+
+
+ View
+ categoryTag:Plug-in Development
+
+
+
+
+ View
+ categoryTag:Plug-in Development
+
+
+
+
+ View
+ categoryTag:Plug-in Development
+
+
+
+
+ View
+ categoryTag:Plug-in Development
+
+
+
+
+ View
+ categoryTag:Plug-in Development
+
+
+
+
+ View
+ categoryTag:General
+
+
+
+
+ View
+ categoryTag:Debug
+
+
+
+
+ View
+ categoryTag:Other
+
+
+
+
+ View
+ categoryTag:Other
+
+
+
+
+ View
+ categoryTag:Other
+
+
+
+
+ View
+ categoryTag:Server
+
+
+
+
+ View
+ categoryTag:XML
+
+
+
+
+ View
+ categoryTag:XML
+
+
+
+
+ View
+ categoryTag:XML
+
+
+
+
+ View
+ categoryTag:XML
+
+
+
+
+ View
+ categoryTag:XML
+
+
+
+ glue
+ move_after:PerspectiveSpacer
+ SHOW_RESTORE_MENU
+
+
+ move_after:Spacer Glue
+ HIDEABLE
+ SHOW_RESTORE_MENU
+
+
+ glue
+ move_after:SearchField
+ SHOW_RESTORE_MENU
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1007707583.index b/.metadata/.plugins/org.eclipse.jdt.core/1007707583.index
deleted file mode 100644
index ec5f85a..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1007707583.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/104128081.index b/.metadata/.plugins/org.eclipse.jdt.core/104128081.index
deleted file mode 100644
index 2f6e1c1..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/104128081.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1124510679.index b/.metadata/.plugins/org.eclipse.jdt.core/1124510679.index
deleted file mode 100644
index 2d2095c..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1124510679.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1136998059.index b/.metadata/.plugins/org.eclipse.jdt.core/1136998059.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1136998059.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1305835383.index b/.metadata/.plugins/org.eclipse.jdt.core/1305835383.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1305835383.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1309346846.index b/.metadata/.plugins/org.eclipse.jdt.core/1309346846.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1309346846.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1317406562.index b/.metadata/.plugins/org.eclipse.jdt.core/1317406562.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1317406562.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1350656539.index b/.metadata/.plugins/org.eclipse.jdt.core/1350656539.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1350656539.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1351491558.index b/.metadata/.plugins/org.eclipse.jdt.core/1351491558.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1351491558.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1462452114.index b/.metadata/.plugins/org.eclipse.jdt.core/1462452114.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1462452114.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1490162601.index b/.metadata/.plugins/org.eclipse.jdt.core/1490162601.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1490162601.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1545642898.index b/.metadata/.plugins/org.eclipse.jdt.core/1545642898.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1545642898.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1550771283.index b/.metadata/.plugins/org.eclipse.jdt.core/1550771283.index
deleted file mode 100644
index 48c52ed..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1550771283.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1578672902.index b/.metadata/.plugins/org.eclipse.jdt.core/1578672902.index
deleted file mode 100644
index fa97256..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1578672902.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1585095811.index b/.metadata/.plugins/org.eclipse.jdt.core/1585095811.index
deleted file mode 100644
index 08502ef..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1585095811.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1608883781.index b/.metadata/.plugins/org.eclipse.jdt.core/1608883781.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1608883781.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/16257995.index b/.metadata/.plugins/org.eclipse.jdt.core/16257995.index
deleted file mode 100644
index 97e5408..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/16257995.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1703316476.index b/.metadata/.plugins/org.eclipse.jdt.core/1703316476.index
deleted file mode 100644
index 5b5a5b7..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1703316476.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1738064530.index b/.metadata/.plugins/org.eclipse.jdt.core/1738064530.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1738064530.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index b/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index
index 01d2b95..0ed5948 100644
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index and b/.metadata/.plugins/org.eclipse.jdt.core/1865797976.index differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/1989349782.index b/.metadata/.plugins/org.eclipse.jdt.core/1989349782.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/1989349782.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2356684838.index b/.metadata/.plugins/org.eclipse.jdt.core/2356684838.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2356684838.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2392911294.index b/.metadata/.plugins/org.eclipse.jdt.core/2392911294.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2392911294.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/24151692.index b/.metadata/.plugins/org.eclipse.jdt.core/24151692.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/24151692.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2479985475.index b/.metadata/.plugins/org.eclipse.jdt.core/2479985475.index
deleted file mode 100644
index d9983f7..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2479985475.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2497820567.index b/.metadata/.plugins/org.eclipse.jdt.core/2497820567.index
deleted file mode 100644
index 067d0fd..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2497820567.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/262304292.index b/.metadata/.plugins/org.eclipse.jdt.core/262304292.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/262304292.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2696219173.index b/.metadata/.plugins/org.eclipse.jdt.core/2696219173.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2696219173.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2722916612.index b/.metadata/.plugins/org.eclipse.jdt.core/2722916612.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2722916612.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2783277246.index b/.metadata/.plugins/org.eclipse.jdt.core/2783277246.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2783277246.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2865784985.index b/.metadata/.plugins/org.eclipse.jdt.core/2865784985.index
deleted file mode 100644
index ce9ef3d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2865784985.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/2893713661.index b/.metadata/.plugins/org.eclipse.jdt.core/2893713661.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/2893713661.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3011908894.index b/.metadata/.plugins/org.eclipse.jdt.core/3011908894.index
deleted file mode 100644
index 5161ca2..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3011908894.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3040618168.index b/.metadata/.plugins/org.eclipse.jdt.core/3040618168.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3040618168.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3075372551.index b/.metadata/.plugins/org.eclipse.jdt.core/3075372551.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3075372551.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3096076814.index b/.metadata/.plugins/org.eclipse.jdt.core/3096076814.index
deleted file mode 100644
index 6c9585d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3096076814.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3190887717.index b/.metadata/.plugins/org.eclipse.jdt.core/3190887717.index
deleted file mode 100644
index 6f6ca96..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3190887717.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3282007833.index b/.metadata/.plugins/org.eclipse.jdt.core/3282007833.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3282007833.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3331268705.index b/.metadata/.plugins/org.eclipse.jdt.core/3331268705.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3331268705.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3390708485.index b/.metadata/.plugins/org.eclipse.jdt.core/3390708485.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3390708485.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3432172071.index b/.metadata/.plugins/org.eclipse.jdt.core/3432172071.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3432172071.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3534421922.index b/.metadata/.plugins/org.eclipse.jdt.core/3534421922.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3534421922.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3591837599.index b/.metadata/.plugins/org.eclipse.jdt.core/3591837599.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3591837599.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3791361740.index b/.metadata/.plugins/org.eclipse.jdt.core/3791361740.index
deleted file mode 100644
index 5219b23..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3791361740.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3901683754.index b/.metadata/.plugins/org.eclipse.jdt.core/3901683754.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3901683754.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3945373644.index b/.metadata/.plugins/org.eclipse.jdt.core/3945373644.index
deleted file mode 100644
index a908c6b..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3945373644.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3950534511.index b/.metadata/.plugins/org.eclipse.jdt.core/3950534511.index
deleted file mode 100644
index 70a1baa..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3950534511.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/3965639856.index b/.metadata/.plugins/org.eclipse.jdt.core/3965639856.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/3965639856.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/4153997317.index b/.metadata/.plugins/org.eclipse.jdt.core/4153997317.index
deleted file mode 100644
index c136a22..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/4153997317.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/4255926859.index b/.metadata/.plugins/org.eclipse.jdt.core/4255926859.index
deleted file mode 100644
index 7429d2a..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/4255926859.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/436234689.index b/.metadata/.plugins/org.eclipse.jdt.core/436234689.index
deleted file mode 100644
index 4dd6727..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/436234689.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/458837781.index b/.metadata/.plugins/org.eclipse.jdt.core/458837781.index
deleted file mode 100644
index 1075d7d..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/458837781.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/606255621.index b/.metadata/.plugins/org.eclipse.jdt.core/606255621.index
deleted file mode 100644
index 9762973..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/606255621.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/734632761.index b/.metadata/.plugins/org.eclipse.jdt.core/734632761.index
deleted file mode 100644
index c1fc277..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/734632761.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/797385254.index b/.metadata/.plugins/org.eclipse.jdt.core/797385254.index
deleted file mode 100644
index 6519dca..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/797385254.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/860469874.index b/.metadata/.plugins/org.eclipse.jdt.core/860469874.index
deleted file mode 100644
index 59c4d33..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/860469874.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/890886771.index b/.metadata/.plugins/org.eclipse.jdt.core/890886771.index
deleted file mode 100644
index 8c8b386..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/890886771.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/900586112.index b/.metadata/.plugins/org.eclipse.jdt.core/900586112.index
deleted file mode 100644
index fad63ef..0000000
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/900586112.index and /dev/null differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache
index ee55e59..015c674 100644
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache and b/.metadata/.plugins/org.eclipse.jdt.core/externalFilesCache differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps b/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps
index 97e9323..731353d 100644
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps and b/.metadata/.plugins/org.eclipse.jdt.core/externalLibsTimeStamps differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache b/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache
index d62d0eb..08dc8e7 100644
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache and b/.metadata/.plugins/org.eclipse.jdt.core/nonChainingJarsCache differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt
index 1da94e9..c3b92e9 100644
--- a/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt
+++ b/.metadata/.plugins/org.eclipse.jdt.core/savedIndexNames.txt
@@ -1,58 +1,65 @@
-INDEX VERSION 1.134+/home/mario/Workspaces/blight/.metadata/.plugins/org.eclipse.jdt.core
-3190887717.index
-3965639856.index
-262304292.index
-3534421922.index
-1585095811.index
-16257995.index
-1350656539.index
-1136998059.index
-458837781.index
-860469874.index
-1305835383.index
-4153997317.index
-1550771283.index
-900586112.index
-3950534511.index
-1462452114.index
-3331268705.index
-1989349782.index
-2722916612.index
-1738064530.index
-1007707583.index
-2497820567.index
-734632761.index
+INDEX VERSION 1.134+C:\Projekte\blight\.metadata\.plugins\org.eclipse.jdt.core
+1730180473.index
+1560896368.index
+3434360348.index
+463881636.index
+2160052426.index
+2563355238.index
+280283746.index
+4260524346.index
+3103640776.index
+342961285.index
+1271954631.index
+2357666455.index
+52047090.index
+2034054389.index
+330211381.index
+2878081145.index
+842794024.index
+3981805811.index
+1210571632.index
+3496211806.index
+1110902487.index
+274839682.index
+563269584.index
+1307459789.index
+2565315493.index
+447892417.index
+746211526.index
+1734210249.index
+1531656712.index
+2352162652.index
+3278945159.index
+1724804096.index
+1680596161.index
+1660620864.index
+993751451.index
+55438146.index
+1228247778.index
+770656589.index
+1962463332.index
+3147751859.index
+1431966402.index
+2109219904.index
1865797976.index
-3591837599.index
-2392911294.index
-1490162601.index
-1124510679.index
-3011908894.index
-2356684838.index
-3390708485.index
-436234689.index
-2865784985.index
-1545642898.index
-24151692.index
-1578672902.index
-2783277246.index
-606255621.index
-797385254.index
-3901683754.index
-2696219173.index
-2479985475.index
-4255926859.index
-1608883781.index
-890886771.index
-104128081.index
-1703316476.index
-3075372551.index
-3791361740.index
-1351491558.index
-1317406562.index
-3096076814.index
-1309346846.index
-3282007833.index
-3432172071.index
-2893713661.index
-3040618168.index
+1340110446.index
+2032725075.index
+2788838607.index
+4079747914.index
+3296889058.index
+633025986.index
+3697082729.index
+2913097348.index
+306663419.index
+404070292.index
+1839710279.index
+3002461991.index
+3721901883.index
+3822973035.index
+887038847.index
+2999631420.index
+2615701196.index
+2049044952.index
+3035175596.index
+1639262426.index
+13457938.index
diff --git a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat
index b2cd4d5..6c69286 100644
Binary files a/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat and b/.metadata/.plugins/org.eclipse.jdt.core/variablesAndContainers.dat differ
diff --git a/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml b/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml
index 2b4cd2c..ec98ae6 100644
--- a/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml
+++ b/.metadata/.plugins/org.eclipse.jdt.launching/.install.xml
@@ -1,5 +1,8 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml b/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml
index ff55ec7..8a97594 100644
--- a/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml
+++ b/.metadata/.plugins/org.eclipse.jdt.launching/libraryInfos.xml
@@ -1,5 +1,28 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml
index a4ee3cb..6cd9562 100644
--- a/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml
+++ b/.metadata/.plugins/org.eclipse.jdt.ui/OpenTypeHistory.xml
@@ -1,2 +1,2 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml
index 9e390f5..8c365b7 100644
--- a/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml
+++ b/.metadata/.plugins/org.eclipse.jdt.ui/QualifiedTypeNameHistory.xml
@@ -1,2 +1,2 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml
index 1618bcd..312100c 100644
--- a/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml
+++ b/.metadata/.plugins/org.eclipse.jdt.ui/dialog_settings.xml
@@ -1,16 +1,16 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml b/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml
index 96a5807..5c579a2 100644
--- a/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml
+++ b/.metadata/.plugins/org.eclipse.linuxtools.docker.core/dockerconnections.xml
@@ -1,4 +1,4 @@
-
-
-
-
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml b/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
index 9effde7..5fa1b75 100644
--- a/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
+++ b/.metadata/.plugins/org.eclipse.m2e.logback/logback.2.7.101.20251017-1242.xml
@@ -1,41 +1,41 @@
-
-
-
- %date [%thread] %-5level %logger{35} - %msg%n
-
-
- ${org.eclipse.m2e.log.console.threshold:-OFF}
-
-
-
-
- ${org.eclipse.m2e.log.dir}/0.log
-
- ${org.eclipse.m2e.log.dir}/%i.log
- 1
- 10
-
-
- 10MB
-
-
- %date [%thread] %-5level %logger{35} - %msg%n
-
-
-
-
-
- WARN
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ %date [%thread] %-5level %logger{35} - %msg%n
+
+
+ ${org.eclipse.m2e.log.console.threshold:-OFF}
+
+
+
+
+ ${org.eclipse.m2e.log.dir}/0.log
+
+ ${org.eclipse.m2e.log.dir}/%i.log
+ 1
+ 10
+
+
+ 10MB
+
+
+ %date [%thread] %-5level %logger{35} - %msg%n
+
+
+
+
+
+ WARN
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup
index 1f73e14..a1ef8f5 100644
--- a/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup
+++ b/.metadata/.plugins/org.eclipse.oomph.setup/workspace.setup
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml
index 5ca0b77..1ef2b05 100644
--- a/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml
+++ b/.metadata/.plugins/org.eclipse.tips.ide/dialog_settings.xml
@@ -1,3 +1,3 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml b/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml
index 50f1edb..e4f30a7 100644
--- a/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml
+++ b/.metadata/.plugins/org.eclipse.ui.editors/dialog_settings.xml
@@ -1,5 +1,5 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.ui.intro/introstate b/.metadata/.plugins/org.eclipse.ui.intro/introstate
index 02f134f..1cc22f5 100644
--- a/.metadata/.plugins/org.eclipse.ui.intro/introstate
+++ b/.metadata/.plugins/org.eclipse.ui.intro/introstate
@@ -1,2 +1,2 @@
-
+
\ No newline at end of file
diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml
index 8e2e4c5..a201ff6 100644
--- a/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml
+++ b/.metadata/.plugins/org.eclipse.ui.workbench/dialog_settings.xml
@@ -1,19 +1,19 @@
-
-
+
+
diff --git a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml
index c9635ed..1e6320b 100644
--- a/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml
+++ b/.metadata/.plugins/org.eclipse.ui.workbench/workingsets.xml
@@ -1,6 +1,6 @@
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties b/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties
index cd7f694..90990f0 100644
--- a/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties
+++ b/.metadata/.plugins/org.eclipse.wst.sse.core/task-tags.properties
@@ -1,3 +1,3 @@
-#
-#Wed May 06 22:30:24 CEST 2026
-task-tag-projects-already-scanned=blight-game
+#
+#Sun May 10 09:46:42 CEST 2026
+task-tag-projects-already-scanned=blight-common,blight-editor,blight,blight-game
diff --git a/.metadata/version.ini b/.metadata/version.ini
index ac8f4f3..a9bc14c 100644
--- a/.metadata/version.ini
+++ b/.metadata/version.ini
@@ -1,3 +1,3 @@
-#Thu May 07 06:27:32 CEST 2026
-org.eclipse.core.runtime=2
-org.eclipse.platform=4.39.0.v20260226-0420
+#Sun May 10 10:13:47 CEST 2026
+org.eclipse.core.runtime=2
+org.eclipse.platform=4.39.0.v20260226-0420
diff --git a/blight-assets/build.gradle b/blight-assets/build.gradle
new file mode 100644
index 0000000..1e2dcc1
--- /dev/null
+++ b/blight-assets/build.gradle
@@ -0,0 +1,5 @@
+// Gemeinsame Spiel-Assets – kein Java-Quellcode, nur Ressourcen.
+// Alle Projekte binden dieses Subprojekt ein, um Zugang zu den
+// geteilten Assets (MatDefs, Shaders, Textures) auf dem Classpath zu erhalten.
+//
+// Editor-spezifische Assets (Tool-Icons etc.) verbleiben in blight-editor.
diff --git a/blight-assets/src/main/resources/MatDefs/Grass.j3md b/blight-assets/src/main/resources/MatDefs/Grass.j3md
new file mode 100644
index 0000000..19f61be
--- /dev/null
+++ b/blight-assets/src/main/resources/MatDefs/Grass.j3md
@@ -0,0 +1,28 @@
+MaterialDef Grass {
+
+ MaterialParameters {
+ Color Color (Color) : 1.0 1.0 1.0 1.0
+ Texture2D ColorMap
+ Float WindSpeed : 0.5
+ Float WindStrength : 0.12
+ }
+
+ Technique {
+ VertexShader GLSL150: Shaders/Grass.vert
+ FragmentShader GLSL150: Shaders/Grass.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ Time
+ }
+
+ RenderState {
+ FaceCull Off
+ }
+
+ Defines {
+ HAS_COLORMAP : ColorMap
+ }
+ }
+}
diff --git a/blight-assets/src/main/resources/MatDefs/Tree.j3md b/blight-assets/src/main/resources/MatDefs/Tree.j3md
new file mode 100644
index 0000000..25e4729
--- /dev/null
+++ b/blight-assets/src/main/resources/MatDefs/Tree.j3md
@@ -0,0 +1,21 @@
+MaterialDef Tree {
+
+ MaterialParameters {
+ Color Diffuse (Color) : 0.42 0.26 0.10 1.0
+ Float WindStrength : 0.15
+ Float WindSpeed : 0.5
+ Texture2D BarkMap
+ Boolean HasBarkMap : false
+ }
+
+ Technique {
+ VertexShader GLSL150 : Shaders/Tree.vert
+ FragmentShader GLSL150 : Shaders/Tree.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ Time
+ }
+ }
+}
diff --git a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md
new file mode 100644
index 0000000..d57e4bd
--- /dev/null
+++ b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md
@@ -0,0 +1,83 @@
+MaterialDef TreeLeaf {
+
+ MaterialParameters {
+ Color Diffuse (Color) : 0.18 0.60 0.10 1.0
+ Float WindStrength : 0.30
+ Float WindSpeed : 0.7
+ Texture2D LeafMap
+ Boolean HasLeafMap : false
+
+ // Vom Shadow-Renderer befüllt (PostShadow-Pass) — vollständige Liste aus PostShadow.j3md
+ Int BoundDrawBuffer
+ Int FilterMode
+ Boolean HardwareShadows
+ Texture2D ShadowMap0
+ Texture2D ShadowMap1
+ Texture2D ShadowMap2
+ Texture2D ShadowMap3
+ Texture2D ShadowMap4
+ Texture2D ShadowMap5
+ Float ShadowIntensity : 1.0
+ Vector4 Splits
+ Vector2 FadeInfo
+ Matrix4 LightViewProjectionMatrix0
+ Matrix4 LightViewProjectionMatrix1
+ Matrix4 LightViewProjectionMatrix2
+ Matrix4 LightViewProjectionMatrix3
+ Matrix4 LightViewProjectionMatrix4
+ Matrix4 LightViewProjectionMatrix5
+ Vector3 LightPos
+ Vector3 LightDir
+ Float PCFEdge
+ Float ShadowMapSize
+ Boolean BackfaceShadows : false
+ }
+
+ Technique {
+ VertexShader GLSL150 : Shaders/Tree.vert
+ FragmentShader GLSL150 : Shaders/TreeLeaf.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ Time
+ }
+
+ RenderState {
+ FaceCull Off
+ }
+ }
+
+ Technique PostShadow {
+ VertexShader GLSL150 : Shaders/LeafPostShadow.vert
+ FragmentShader GLSL150 : Shaders/LeafPostShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ }
+
+ ForcedRenderState {
+ Blend Modulate
+ FaceCull Off
+ DepthWrite Off
+ }
+ }
+
+ Technique PreShadow {
+ VertexShader GLSL150 : Shaders/LeafPreShadow.vert
+ FragmentShader GLSL150 : Shaders/LeafPreShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+
+ ForcedRenderState {
+ FaceCull Off
+ DepthTest On
+ DepthWrite On
+ PolyOffset 5 3
+ ColorWrite Off
+ }
+ }
+}
diff --git a/blight-assets/src/main/resources/Shaders/Grass.frag b/blight-assets/src/main/resources/Shaders/Grass.frag
new file mode 100644
index 0000000..3fbc02f
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/Grass.frag
@@ -0,0 +1,18 @@
+uniform vec4 m_Color;
+
+#ifdef HAS_COLORMAP
+uniform sampler2D m_ColorMap;
+#endif
+
+in vec2 texCoord;
+out vec4 outFragColor;
+
+void main() {
+ vec4 color = m_Color;
+#ifdef HAS_COLORMAP
+ color *= texture(m_ColorMap, texCoord);
+#endif
+ // Alpha-Discard: transparente Pixel sofort verwerfen, Z-Buffer bleibt sauber
+ if (color.a < 0.5) discard;
+ outFragColor = color;
+}
diff --git a/blight-assets/src/main/resources/Shaders/Grass.vert b/blight-assets/src/main/resources/Shaders/Grass.vert
new file mode 100644
index 0000000..49ba07d
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/Grass.vert
@@ -0,0 +1,33 @@
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldMatrix;
+uniform float g_Time;
+
+uniform float m_WindSpeed;
+uniform float m_WindStrength;
+
+in vec3 inPosition;
+in vec2 inTexCoord;
+
+out vec2 texCoord;
+
+void main() {
+ vec4 pos = vec4(inPosition, 1.0);
+
+ // Nur obere Hälfte des Halms bewegen (inTexCoord.y = 0 unten, 1 oben)
+ if (inTexCoord.y > 0.05) {
+ vec2 worldXZ = (g_WorldMatrix * pos).xz;
+ float t = g_Time * m_WindSpeed;
+
+ // Zwei überlagerte Sinuswellen für organische Bewegung
+ float sway = sin(t * 2.1 + worldXZ.x * 0.08 + worldXZ.y * 0.06) * 0.6
+ + sin(t * 1.4 - worldXZ.x * 0.05 + worldXZ.y * 0.09) * 0.4;
+
+ // Quadratische Gewichtung: Spitze bewegt sich mehr als Basis
+ float bend = sway * m_WindStrength * inTexCoord.y * inTexCoord.y;
+ pos.x += bend;
+ pos.z += bend * 0.3;
+ }
+
+ texCoord = inTexCoord;
+ gl_Position = g_WorldViewProjectionMatrix * pos;
+}
diff --git a/blight-assets/src/main/resources/Shaders/LeafPostShadow.frag b/blight-assets/src/main/resources/Shaders/LeafPostShadow.frag
new file mode 100644
index 0000000..f87574b
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/LeafPostShadow.frag
@@ -0,0 +1,20 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform sampler2D m_ShadowMap0;
+uniform float m_ShadowIntensity;
+uniform sampler2D m_LeafMap;
+uniform bool m_HasLeafMap;
+
+in vec4 shadowCoord;
+in vec2 texCoord;
+
+void main() {
+ // Transparente Blattbereiche empfangen keinen Schatten
+ if (m_HasLeafMap && texture2D(m_LeafMap, texCoord).a < 0.5) discard;
+
+ vec3 coord = shadowCoord.xyz / shadowCoord.w;
+ float mapDepth = texture2D(m_ShadowMap0, coord.xy).r;
+ float lit = (coord.z > mapDepth + 0.001) ? (1.0 - m_ShadowIntensity) : 1.0;
+
+ gl_FragColor = vec4(lit, lit, lit, 1.0);
+}
diff --git a/blight-assets/src/main/resources/Shaders/LeafPostShadow.vert b/blight-assets/src/main/resources/Shaders/LeafPostShadow.vert
new file mode 100644
index 0000000..5b13bb8
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/LeafPostShadow.vert
@@ -0,0 +1,18 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldMatrix;
+uniform mat4 m_LightViewProjectionMatrix0;
+
+in vec3 inPosition;
+in vec2 inTexCoord;
+
+out vec4 shadowCoord;
+out vec2 texCoord;
+
+void main() {
+ gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
+ vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
+ shadowCoord = m_LightViewProjectionMatrix0 * worldPos;
+ texCoord = inTexCoord;
+}
diff --git a/blight-assets/src/main/resources/Shaders/LeafPreShadow.frag b/blight-assets/src/main/resources/Shaders/LeafPreShadow.frag
new file mode 100644
index 0000000..59a31e7
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/LeafPreShadow.frag
@@ -0,0 +1,15 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform sampler2D m_LeafMap;
+uniform bool m_HasLeafMap;
+
+in vec2 texCoord;
+
+void main() {
+ if (m_HasLeafMap) {
+ vec4 tex = texture2D(m_LeafMap, texCoord);
+ if (tex.a < 0.5) discard;
+ }
+ // Nur Tiefe schreiben — ColorWrite ist per ForcedRenderState deaktiviert
+ gl_FragColor = vec4(1.0);
+}
diff --git a/blight-assets/src/main/resources/Shaders/LeafPreShadow.vert b/blight-assets/src/main/resources/Shaders/LeafPreShadow.vert
new file mode 100644
index 0000000..8b60506
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/LeafPreShadow.vert
@@ -0,0 +1,13 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform mat4 g_WorldViewProjectionMatrix;
+
+in vec3 inPosition;
+in vec2 inTexCoord;
+
+out vec2 texCoord;
+
+void main() {
+ gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
+ texCoord = inTexCoord;
+}
diff --git a/blight-assets/src/main/resources/Shaders/PreShadow.frag b/blight-assets/src/main/resources/Shaders/PreShadow.frag
new file mode 100644
index 0000000..48e9312
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/PreShadow.frag
@@ -0,0 +1,17 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform sampler2D m_LeafMap;
+uniform float m_AlphaDiscardThreshold;
+varying vec2 texCoord;
+
+void main() {
+ #ifdef HAS_LEAFMAP
+ float alpha = texture2D(m_LeafMap, texCoord).a;
+ if (alpha < m_AlphaDiscardThreshold) {
+ discard;
+ }
+ #endif
+
+ // Wir schreiben nur die Tiefe, die Farbe ist egal.
+ gl_FragColor = vec4(1.0);
+}
\ No newline at end of file
diff --git a/blight-assets/src/main/resources/Shaders/Tree.frag b/blight-assets/src/main/resources/Shaders/Tree.frag
new file mode 100644
index 0000000..4232df7
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/Tree.frag
@@ -0,0 +1,29 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform vec4 m_Diffuse;
+uniform sampler2D m_BarkMap;
+uniform bool m_HasBarkMap;
+
+in vec2 texCoord;
+in vec3 worldNormal;
+
+void main() {
+ vec3 n = normalize(worldNormal);
+
+ // Sun at ~45° elevation from SE — good contrast on vertical cylinders
+ vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4));
+ vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4));
+ vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7));
+
+ float sun = max(dot(n, sunDir), 0.0);
+ float fill = max(dot(n, fillDir), 0.0) * 0.22;
+ float rim = max(dot(n, rimDir), 0.0) * 0.14;
+ float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; // [0.0, 0.8]
+ float light = sun * 0.75 + fill + rim + sky * 0.09 + 0.05;
+
+ vec3 baseColor = m_HasBarkMap
+ ? texture2D(m_BarkMap, texCoord).rgb * m_Diffuse.rgb
+ : m_Diffuse.rgb;
+
+ gl_FragColor = vec4(baseColor * clamp(light, 0.0, 1.0), m_Diffuse.a);
+}
diff --git a/blight-assets/src/main/resources/Shaders/Tree.vert b/blight-assets/src/main/resources/Shaders/Tree.vert
new file mode 100644
index 0000000..a8da904
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/Tree.vert
@@ -0,0 +1,33 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldMatrix;
+uniform float g_Time;
+uniform float m_WindStrength;
+uniform float m_WindSpeed;
+
+in vec3 inPosition;
+in vec3 inNormal;
+in vec2 inTexCoord;
+in vec4 inColor; // R = Wind-Gewicht (0 = Wurzel, 1 = Spitze)
+
+out vec2 texCoord;
+out vec3 worldNormal;
+
+void main() {
+float windW = inColor.r;
+float t = g_Time * m_WindSpeed;
+
+// Welt-Position für orts-abhängige Phase (verhindert synchrones Schwingen)
+vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
+float phase = worldPos.x * 0.08 + worldPos.z * 0.06;
+float swayX = sin(t + phase) * windW * m_WindStrength;
+float swayZ = cos(t * 0.73 + phase) * windW * m_WindStrength * 0.55;
+
+vec3 animPos = inPosition + vec3(swayX, 0.0, swayZ);
+
+gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0);
+texCoord = inTexCoord;
+worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz);
+}
\ No newline at end of file
diff --git a/blight-assets/src/main/resources/Shaders/TreeLeaf.frag b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag
new file mode 100644
index 0000000..d805691
--- /dev/null
+++ b/blight-assets/src/main/resources/Shaders/TreeLeaf.frag
@@ -0,0 +1,27 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform vec4 m_Diffuse;
+uniform sampler2D m_LeafMap;
+uniform bool m_HasLeafMap;
+
+in vec2 texCoord;
+in vec3 worldNormal;
+
+void main() {
+ vec3 baseColor;
+
+ if (m_HasLeafMap) {
+ vec4 tex = texture2D(m_LeafMap, texCoord);
+ if (tex.a < 0.5) discard;
+ baseColor = tex.rgb * m_Diffuse.rgb;
+ } else {
+ // Fallback: kreisförmiger Clip
+ vec2 uv = texCoord * 2.0 - 1.0;
+ if (dot(uv, uv) > 0.95) discard;
+ float edge = 1.0 - dot(uv, uv);
+ baseColor = m_Diffuse.rgb * (0.7 + 0.3 * edge);
+ }
+
+ // Leaves transmit light — no directional shading, uniform brightness
+ gl_FragColor = vec4(baseColor, 1.0);
+}
diff --git a/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg
new file mode 100644
index 0000000..6277b94
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark001_Color.jpg differ
diff --git a/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg
new file mode 100644
index 0000000..b2525e9
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark002_Color.jpg differ
diff --git a/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg
new file mode 100644
index 0000000..15ed21b
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark003_Color.jpg differ
diff --git a/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg b/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg
new file mode 100644
index 0000000..43ee658
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark008_Color.jpg differ
diff --git a/blight-game/assets/Textures/gras.png b/blight-assets/src/main/resources/Textures/gras.png
similarity index 100%
rename from blight-game/assets/Textures/gras.png
rename to blight-assets/src/main/resources/Textures/gras.png
diff --git a/blight-assets/src/main/resources/Textures/leaves/ash.png b/blight-assets/src/main/resources/Textures/leaves/ash.png
new file mode 100644
index 0000000..7f7c844
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/ash.png differ
diff --git a/blight-assets/src/main/resources/Textures/leaves/aspen.png b/blight-assets/src/main/resources/Textures/leaves/aspen.png
new file mode 100644
index 0000000..89265a9
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/aspen.png differ
diff --git a/blight-assets/src/main/resources/Textures/leaves/oak.png b/blight-assets/src/main/resources/Textures/leaves/oak.png
new file mode 100644
index 0000000..061fbf6
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/oak.png differ
diff --git a/blight-assets/src/main/resources/Textures/leaves/palm.png b/blight-assets/src/main/resources/Textures/leaves/palm.png
new file mode 100644
index 0000000..b6db0bb
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/palm.png differ
diff --git a/blight-assets/src/main/resources/Textures/leaves/palm2.png b/blight-assets/src/main/resources/Textures/leaves/palm2.png
new file mode 100644
index 0000000..3ca0657
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/palm2.png differ
diff --git a/blight-assets/src/main/resources/Textures/leaves/pine.png b/blight-assets/src/main/resources/Textures/leaves/pine.png
new file mode 100644
index 0000000..478dca3
Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/pine.png differ
diff --git a/blight-common/.gitignore b/blight-common/.gitignore
new file mode 100644
index 0000000..84c048a
--- /dev/null
+++ b/blight-common/.gitignore
@@ -0,0 +1 @@
+/build/
diff --git a/blight-common/build.gradle b/blight-common/build.gradle
new file mode 100644
index 0000000..340ff37
--- /dev/null
+++ b/blight-common/build.gradle
@@ -0,0 +1,20 @@
+// Gemeinsames Datenmodell — wird von blight-editor und blight-game eingebunden.
+// Selbst-ständig konfiguriert, damit es auch funktioniert wenn blight-editor
+// oder blight-game standalone in Eclipse importiert werden (ohne Root-Build).
+plugins {
+ id 'java'
+}
+
+group = 'de.blight'
+version = '0.1.0'
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+compileJava.options.encoding = 'UTF-8'
+
+repositories {
+ mavenCentral()
+}
diff --git a/blight-common/src/main/java/de/blight/common/MapData.java b/blight-common/src/main/java/de/blight/common/MapData.java
new file mode 100644
index 0000000..76fe94d
--- /dev/null
+++ b/blight-common/src/main/java/de/blight/common/MapData.java
@@ -0,0 +1,68 @@
+package de.blight.common;
+
+/**
+ * Serialisierbarer Zustand einer Blight-Weltkarte.
+ *
+ * Basis-Terrain : 4097 × 4097 Vertices (= 4096 × 4096 Zellen),
+ * 8 Welteinheiten pro Zelle → Welt −2048 .. +2048.
+ * Obere Schicht : 513 × 513 Vertices (= 512 × 512 Zellen), gleiche Weltausdehnung.
+ * Splatmap : 513 × 513 Pixel (passt auf Spiel-Terrain 1:1).
+ * Kanäle R/G/B = Gewicht für Tex2/Tex3/Tex4; Tex1 füllt den Rest.
+ */
+public final class MapData {
+
+ // ── Terrain-Konstanten ────────────────────────────────────────────────────
+
+ /** Vertices pro Achse des Basis-Terrains (muss 2^n + 1 sein). */
+ public static final int TERRAIN_VERTS = 4097;
+
+ // ── Upper-Layer-Konstanten ────────────────────────────────────────────────
+
+ /** Zellen pro Achse der oberen Schicht. */
+ public static final int UPPER_CELLS = 512;
+
+ /** Vertices pro Achse der oberen Schicht (UPPER_CELLS + 1). */
+ public static final int UPPER_VERTS = 513;
+
+ // ── Splatmap-Konstanten ────────────────────────────────────────────────────
+
+ /** Pixel pro Achse der Splatmap (entspricht UPPER_VERTS = Spiel-Terrain-Auflösung). */
+ public static final int SPLAT_SIZE = 513;
+
+ // ── Daten ─────────────────────────────────────────────────────────────────
+
+ /** Y-Höhe jedes Vertex im Basis-Terrain [TERRAIN_VERTS²]. */
+ public final float[] terrainHeight;
+
+ /** Y der oberen Oberfläche der Bergschicht [UPPER_VERTS²]. */
+ public final float[] upperTop;
+
+ /** Y der Höhlendecke [UPPER_VERTS²]. */
+ public final float[] upperBottom;
+
+ /** 1 = Loch (offen), 0 = massiv [UPPER_CELLS²]. */
+ public final byte[] upperHole;
+
+ /** Splatmap Rot-Kanal: Tex1-Helligkeit (Alpha.R), immer 255 [SPLAT_SIZE²]. */
+ public final byte[] splatR;
+
+ /** Splatmap Grün-Kanal: Tex2 (Fels) mix-Faktor (Alpha.G) [SPLAT_SIZE²], Bytes 0–255. */
+ public final byte[] splatG;
+
+ /** Splatmap Blau-Kanal: Tex3 (Erde) mix-Faktor (Alpha.B) [SPLAT_SIZE²], Bytes 0–255. */
+ public final byte[] splatB;
+
+ /** Gras-Dichte [SPLAT_SIZE²], Bytes 0–255 (0=kein Gras, 255=max Dichte). */
+ public final byte[] grassDensity;
+
+ public MapData() {
+ terrainHeight = new float[TERRAIN_VERTS * TERRAIN_VERTS];
+ upperTop = new float[UPPER_VERTS * UPPER_VERTS];
+ upperBottom = new float[UPPER_VERTS * UPPER_VERTS];
+ upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
+ splatR = new byte [SPLAT_SIZE * SPLAT_SIZE];
+ splatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
+ splatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
+ grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
+ }
+}
diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java
new file mode 100644
index 0000000..941c7cf
--- /dev/null
+++ b/blight-common/src/main/java/de/blight/common/MapIO.java
@@ -0,0 +1,102 @@
+package de.blight.common;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.file.*;
+import java.util.zip.*;
+
+/**
+ * Liest und schreibt {@link MapData} als komprimierte Binärdatei.
+ *
+ * Speicherort: {@code world/blight_map.blm} relativ zum Arbeitsverzeichnis.
+ * Beide Projekte setzen {@code workingDir = rootDir} im Gradle-Run-Task,
+ * zeigen also auf dasselbe Verzeichnis.
+ *
+ * Versionen:
+ * 1 – Basis-Terrain + Obere Schicht (kein Splatmap)
+ * 2 – wie 1 + Splatmap (R/G/B je 513×513 Bytes)
+ * 3 – wie 2 + Gras-Dichte (513×513 Bytes)
+ */
+public final class MapIO {
+
+ private static final Path MAP_PATH = Paths.get("world", "blight_map.blm");
+
+ private static final int MAGIC = 0x424C4947; // "BLIG"
+ private static final int VERSION = 3;
+
+ private MapIO() {}
+
+ // ── Public API ────────────────────────────────────────────────────────────
+
+ public static boolean exists() {
+ return Files.exists(MAP_PATH);
+ }
+
+ public static Path getMapPath() {
+ return MAP_PATH.toAbsolutePath();
+ }
+
+ public static void save(MapData data) throws IOException {
+ Files.createDirectories(MAP_PATH.getParent());
+ try (DataOutputStream out = new DataOutputStream(
+ new BufferedOutputStream(
+ new GZIPOutputStream(Files.newOutputStream(MAP_PATH))))) {
+ out.writeInt(MAGIC);
+ out.writeInt(VERSION);
+ writeFloats(out, data.terrainHeight);
+ writeFloats(out, data.upperTop);
+ writeFloats(out, data.upperBottom);
+ out.write(data.upperHole);
+ // v2: splatmap
+ out.write(data.splatR);
+ out.write(data.splatG);
+ out.write(data.splatB);
+ // v3: gras-dichte
+ out.write(data.grassDensity);
+ }
+ }
+
+ public static MapData load() throws IOException {
+ MapData data = new MapData();
+ try (DataInputStream in = new DataInputStream(
+ new BufferedInputStream(
+ new GZIPInputStream(Files.newInputStream(MAP_PATH))))) {
+ int magic = in.readInt();
+ int version = in.readInt();
+ if (magic != MAGIC) throw new IOException("Ungültige Map-Datei (falscher Magic)");
+ if (version < 1 || version > VERSION)
+ throw new IOException("Unbekannte Map-Version: " + version);
+
+ readFloats(in, data.terrainHeight);
+ readFloats(in, data.upperTop);
+ readFloats(in, data.upperBottom);
+ in.readFully(data.upperHole);
+
+ if (version >= 2) {
+ in.readFully(data.splatR);
+ in.readFully(data.splatG);
+ in.readFully(data.splatB);
+ }
+ if (version >= 3) {
+ in.readFully(data.grassDensity);
+ }
+ // version 1/2: grassDensity stays all-zeros (= kein Gras)
+ }
+ return data;
+ }
+
+ // ── Hilfsmethoden ─────────────────────────────────────────────────────────
+
+ private static void writeFloats(DataOutputStream out, float[] arr) throws IOException {
+ ByteBuffer buf = ByteBuffer.allocate(arr.length * Float.BYTES)
+ .order(ByteOrder.BIG_ENDIAN);
+ buf.asFloatBuffer().put(arr);
+ out.write(buf.array());
+ }
+
+ private static void readFloats(DataInputStream in, float[] arr) throws IOException {
+ byte[] bytes = new byte[arr.length * Float.BYTES];
+ in.readFully(bytes);
+ ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(arr);
+ }
+}
diff --git a/blight-editor/.gitignore b/blight-editor/.gitignore
new file mode 100644
index 0000000..abc78ba
--- /dev/null
+++ b/blight-editor/.gitignore
@@ -0,0 +1,2 @@
+/.gradle/
+/build/
diff --git a/blight-editor/build.gradle b/blight-editor/build.gradle
index 57c0856..87e06e8 100644
--- a/blight-editor/build.gradle
+++ b/blight-editor/build.gradle
@@ -1,66 +1,66 @@
-plugins {
- id 'java'
- id 'application'
- id 'org.openjfx.javafxplugin' version '0.1.0'
-}
-
-group = 'de.blight'
-version = '0.1.0'
-
-java {
- sourceCompatibility = JavaVersion.VERSION_21
- targetCompatibility = JavaVersion.VERSION_21
-}
-
-javafx {
- version = '21'
- modules = ['javafx.controls', 'javafx.swing']
-}
-
-application {
- mainClass = 'de.blight.editor.EditorLauncher'
- applicationDefaultJvmArgs = [
- '--add-opens', 'java.base/java.lang=ALL-UNNAMED',
- '--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
- '-Djava.library.path=${rootDir}/build/natives',
- ]
-}
-
-repositories {
- mavenCentral()
-}
-
-ext {
- jmeVersion = '3.7.0-stable'
-}
-
-dependencies {
- implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
-}
-
-tasks.register('extractNatives', Copy) {
- def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
- .resolvedArtifacts
- .findAll { it.name.contains('natives') }
- .collect { zipTree(it.file) }
-
- from nativeConf
- into "${buildDir}/natives"
- duplicatesStrategy = DuplicatesStrategy.INCLUDE
-}
-
-run {
- dependsOn extractNatives
- workingDir = rootDir
-}
-
-jar {
- manifest {
- attributes 'Main-Class': application.mainClass
- }
-}
+// group / version / java / repositories kommen vom Root-Build.
+plugins {
+ id 'application'
+ id 'org.openjfx.javafxplugin' version '0.1.0'
+}
+
+javafx {
+ version = '26'
+ modules = ['javafx.controls', 'javafx.swing']
+}
+
+application {
+ mainClass = 'de.blight.editor.EditorLauncher'
+ applicationDefaultJvmArgs = [
+ '--add-opens', 'java.base/java.lang=ALL-UNNAMED',
+ '--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
+ "-Djava.library.path=${buildDir}/natives",
+ ]
+}
+
+ext {
+ jmeVersion = '3.9.0-stable'
+}
+
+dependencies {
+ implementation project(':blight-common')
+ implementation project(':blight-assets')
+ implementation project(':ez-tree-jme')
+
+ implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
+}
+
+tasks.register('extractNatives', Copy) {
+ def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
+ .resolvedArtifacts
+ .findAll { it.name.contains('natives') }
+ .collect { zipTree(it.file) }
+
+ from nativeConf
+ into "${buildDir}/natives"
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+}
+
+sourceSets {
+ main {
+ resources {
+ srcDirs = ['src/main/resources']
+ }
+ }
+}
+
+run {
+ dependsOn extractNatives
+ workingDir = rootDir // gemeinsames Arbeitsverzeichnis = Projekt-Root
+}
+
+jar {
+ manifest {
+ attributes 'Main-Class': application.mainClass
+ }
+}
diff --git a/blight-editor/gradle/wrapper/gradle-wrapper.properties b/blight-editor/gradle/wrapper/gradle-wrapper.properties
index b82aa23..a034286 100644
--- a/blight-editor/gradle/wrapper/gradle-wrapper.properties
+++ b/blight-editor/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/blight-editor/gradlew b/blight-editor/gradlew
index 97de990..30553ae 100755
--- a/blight-editor/gradlew
+++ b/blight-editor/gradlew
@@ -1,249 +1,249 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-#
-# Gradle start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh Gradle
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- if ! command -v java >/dev/null 2>&1
- then
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-# and any embedded shellness will be escaped.
-# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-# treated as '${Hostname}' itself on the command line.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/blight-editor/gradlew.bat b/blight-editor/gradlew.bat
index 16e26a1..66f1aa7 100644
--- a/blight-editor/gradlew.bat
+++ b/blight-editor/gradlew.bat
@@ -1,92 +1,92 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/blight-editor/settings.gradle b/blight-editor/settings.gradle
index 485407c..5f92c16 100644
--- a/blight-editor/settings.gradle
+++ b/blight-editor/settings.gradle
@@ -1 +1,11 @@
-rootProject.name = 'blight-editor'
+rootProject.name = 'blight-editor'
+
+// Sibling-Projekte einbinden.
+// Funktioniert sowohl wenn dieses Projekt direkt in Eclipse importiert wird
+// als auch im übergeordneten Multi-Projekt-Build (dort werden diese Zeilen ignoriert,
+// da die Projekte bereits vom Root-settings.gradle bekannt sind).
+include 'blight-common'
+project(':blight-common').projectDir = new File(settingsDir, '../blight-common')
+
+include 'blight-assets'
+project(':blight-assets').projectDir = new File(settingsDir, '../blight-assets')
diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java
index 000b4a7..0ece3bd 100644
--- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java
+++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java
@@ -1,22 +1,42 @@
package de.blight.editor;
+import de.blight.editor.tool.ChoiceToolParameter;
+import de.blight.editor.tool.EditorTool;
+import de.blight.editor.tool.ToolParameter;
+import de.blight.editor.tree.PalmOptions;
+import de.blight.editor.tree.TreeParams;
+import de.blight.eztree.Billboard;
+import de.blight.eztree.TreeOptions;
+import de.blight.eztree.TreePresets;
+import de.blight.eztree.TreeType;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
+import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
+import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
+import java.net.URL;
import java.nio.file.*;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
public class EditorApp extends Application {
@@ -24,97 +44,1119 @@ public class EditorApp extends Application {
static final int VP_WIDTH = 1024;
static final int VP_HEIGHT = 640;
- // ── Asset-Verzeichnis ────────────────────────────────────────────────────
private static final Path ASSET_ROOT = Paths.get("editor-assets");
- private final SharedInput input = new SharedInput();
+ private final SharedInput input = new SharedInput();
private WritableImage jfxImage;
private ImageView viewport;
private Label statusLabel;
+ private VBox toolPanel;
+ private BorderPane root;
+ private VBox assetPanel;
+ private StackPane worldViewport;
+ private VBox topBar; // MenuBar + aktuelle Toolbar
+ private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons)
+ private final TextField treeNameField = new TextField("Baum1");
+ private ImageView treePreviewView; // aktualisiert wenn JME3 Bild neu erstellt
+ private Stage primaryStage;
+
+ // Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt)
+ private TreeParams treeParams = TreeParams.oak();
+
+ // EZ-Tree-Zustand
+ private TreeOptions ezTreeOptions = TreePresets.oakMedium();
+ private final TextField ezTreeNameField = new TextField("EzBaum1");
+
+ // Palmen-Generator-Zustand
+ private PalmOptions palmOptions = new PalmOptions();
+ private final TextField palmNameField = new TextField("Palme1");
+
+ // Asset-Panel: Pfad-Map + DnD-Zustand
+ private final Map, Path> itemPaths = new HashMap<>();
+ private TreeItem draggedItem = null;
+
+ // Objekt-Werkzeug-Zustand
+ private Label objModelLabel; // zeigt den ausgewählten Modell-Pfad
+ private Label objPosLabel; // zeigt Position/Rotation
+ private CheckBox objSolidCB; // Solid-Flag des selektierten Objekts
+ private CheckBox multiPlaceCB; // Mehrfach-Platzierungs-Modus
+
+ // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein)
+ private ToggleButton objPlaceBtn;
+ private ToggleButton objEditBtn;
+
+ // Mesh-Primitiv-Auswahl (Platzieren-Modus)
+ private ToggleGroup meshToggleGroup;
// Drag-Tracking für Kamerarotation (mittlere Taste)
private double prevDragX, prevDragY;
- // ── Asset-Tree-Items ─────────────────────────────────────────────────────
+ // Drag-Tracking für Gizmo im Objekt-Modus
+ private double objDragPrevX, objDragPrevY;
+ private boolean objDragging = false;
+
+ // Edit-Tracking
+ private double editPressX, editPressY;
+ private int editPressAction;
+ private javafx.animation.Timeline editTimer;
+
+ // Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein)
private TreeItem modelsNode;
private TreeItem texturesNode;
private TreeItem audioNode;
// ── JavaFX Entry-Point ───────────────────────────────────────────────────
- public static void main(String[] args) {
- launch(args);
- }
+ public static void main(String[] args) { launch(args); }
@Override
public void start(Stage stage) {
- // WritableImage für den JME3-Frame-Export
jfxImage = new WritableImage(VP_WIDTH, VP_HEIGHT);
-
- // JME3 starten (Hintergrund-Daemon-Thread)
JmeEditorApp.launch(input, jfxImage, VP_WIDTH, VP_HEIGHT);
- // UI zusammenbauen
- BorderPane root = new BorderPane();
- root.setTop(buildTop());
- root.setLeft(buildAssetPanel());
- root.setCenter(buildViewport());
+ assetPanel = buildAssetPanel();
+ toolPanel = buildToolPanel();
+
+ root = new BorderPane();
+ topBar = buildTop();
+ root.setTop(topBar);
+ worldViewport = buildViewport();
+ root.setLeft(assetPanel);
+ root.setCenter(worldViewport);
+ root.setRight(toolPanel);
root.setBottom(buildStatusBar());
Scene scene = new Scene(root, 1280, 760);
scene.setOnKeyPressed(e -> handleKeyPress(e.getCode(), true));
scene.setOnKeyReleased(e -> handleKeyPress(e.getCode(), false));
+ primaryStage = stage;
stage.setTitle("Blight World Editor");
stage.setScene(scene);
stage.setMinWidth(900);
stage.setMinHeight(600);
stage.setOnCloseRequest(e -> 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); }
+
+ String treeMsg = input.treeGenStatusMsg;
+ if (treeMsg != null) { input.treeGenStatusMsg = null; setStatus(treeMsg); }
+
+ if (input.treePreviewResized) {
+ input.treePreviewResized = false;
+ if (treePreviewView != null)
+ treePreviewView.setImage(input.treePreviewImage);
+ }
+
+ if (input.refreshAssets) {
+ input.refreshAssets = false;
+ refreshCategoryNode(modelsNode,
+ ".j3o", ".obj", ".fbx", ".gltf", ".glb");
+ modelsNode.setExpanded(true);
+ }
+
+ if (input.objectJustPlaced) {
+ input.objectJustPlaced = false;
+ if (multiPlaceCB == null || !multiPlaceCB.isSelected()) {
+ input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT;
+ if (objEditBtn != null) objEditBtn.setSelected(true);
+ root.setRight(buildObjectEditPanel());
+ }
+ }
+
+ if (input.objectSelectionChanged) {
+ input.objectSelectionChanged = false;
+ updateObjectPanel(input.selectedObjectInfo);
+ }
+ })
+ );
+ statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE);
+ statusPoller.play();
+ }
+
+ // ── Modus-Wechsel ────────────────────────────────────────────────────────
+
+ private void switchToTreeGenerator() {
+ topBar.getChildren().set(1, buildTreeToolBar());
+ root.setCenter(buildTreePreviewPanel());
+ root.setRight(buildTreeParamsPanel());
+ }
+
+ private void switchToWorldEditor() {
+ topBar.getChildren().set(1, worldToolBar);
+ root.setCenter(worldViewport);
+ root.setRight(toolPanel);
+ }
+
+ private void switchToEzTree() {
+ topBar.getChildren().set(1, buildEzTreeToolBar());
+ root.setCenter(buildTreePreviewPanel()); // teilt Vorschau-Infrastruktur
+ root.setRight(buildEzTreeParamsPanel());
+ }
+
+ private void switchToPalm() {
+ topBar.getChildren().set(1, buildPalmToolBar());
+ root.setCenter(buildTreePreviewPanel());
+ root.setRight(buildPalmParamsPanel());
}
// ── Oberer Bereich: MenuBar + ToolBar ────────────────────────────────────
private VBox buildTop() {
- // Menüleiste
MenuBar menuBar = new MenuBar();
+
Menu fileMenu = new Menu("Datei");
MenuItem newItem = new MenuItem("Neue Karte");
MenuItem saveItem = new MenuItem("Speichern");
+ saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S"));
+ saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); });
fileMenu.getItems().addAll(newItem, saveItem);
+
+ Menu toolsMenu = new Menu("Werkzeuge");
+ MenuItem treeGenItem = new MenuItem("Baum Generator (blight)");
+ MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)");
+ MenuItem palmItem = new MenuItem("Baum Generator (Palme)");
+ MenuItem worldEditItem = new MenuItem("Welteneditor");
+ treeGenItem.setOnAction(e -> switchToTreeGenerator());
+ ezTreeItem.setOnAction(e -> switchToEzTree());
+ palmItem.setOnAction(e -> switchToPalm());
+ worldEditItem.setOnAction(e -> switchToWorldEditor());
+ toolsMenu.getItems().addAll(treeGenItem, ezTreeItem, palmItem, worldEditItem);
+
Menu viewMenu = new Menu("Ansicht");
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
- resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); // noop, Kamera reset via SharedInput wäre aufwändiger
+ resetCam.setOnAction(e -> input.addMouseDelta(0, 0));
viewMenu.getItems().add(resetCam);
- menuBar.getMenus().addAll(fileMenu, viewMenu);
- // Werkzeugleiste
+ menuBar.getMenus().addAll(fileMenu, toolsMenu, viewMenu);
+
ToolBar toolBar = new ToolBar();
- Button heightTool = new Button("▲▼ Höhe");
- heightTool.setStyle("-fx-font-weight:bold;");
- heightTool.setTooltip(new Tooltip(
- "Linksklick: Terrain anheben\nRechtsklick: Terrain absenken"));
+ ToggleButton baseBtn = new ToggleButton("▲▼ Basis-Terrain");
+ ToggleButton upperBtn = new ToggleButton("⛰ Obere Schicht");
+ ToggleButton holesBtn = new ToggleButton("⬤ Höhlen/Löcher");
+ ToggleButton grassBtn = new ToggleButton("🌿 Gras");
+ ToggleButton textureBtn = new ToggleButton("🎨 Textur");
+ objPlaceBtn = new ToggleButton("📦 Platzieren");
+ objEditBtn = new ToggleButton("🔧 Bearbeiten");
+ baseBtn.setStyle("-fx-font-weight:bold;");
+ upperBtn.setStyle("-fx-font-weight:bold;");
+ holesBtn.setStyle("-fx-font-weight:bold;");
+ grassBtn.setStyle("-fx-font-weight:bold;");
+ textureBtn.setStyle("-fx-font-weight:bold;");
+ objPlaceBtn.setStyle("-fx-font-weight:bold;");
+ objEditBtn.setStyle("-fx-font-weight:bold;");
- Separator sep1 = new Separator(Orientation.VERTICAL);
+ ToggleGroup layerGroup = new ToggleGroup();
+ baseBtn.setToggleGroup(layerGroup);
+ upperBtn.setToggleGroup(layerGroup);
+ holesBtn.setToggleGroup(layerGroup);
+ grassBtn.setToggleGroup(layerGroup);
+ textureBtn.setToggleGroup(layerGroup);
+ objPlaceBtn.setToggleGroup(layerGroup);
+ objEditBtn.setToggleGroup(layerGroup);
+ baseBtn.setSelected(true);
- Label brushLabel = new Label("Pinselstärke:");
- Slider brushSlider = new Slider(0.1, 1.0, 1.0);
- brushSlider.setPrefWidth(100);
- brushSlider.setMajorTickUnit(0.5);
- brushSlider.setShowTickMarks(true);
+ baseBtn.setOnAction(e -> {
+ input.activeLayer = 0; input.activeTool = input.heightTool;
+ root.setRight(toolPanel);
+ showToolParameters(toolPanel, input.activeTool);
+ });
+ upperBtn.setOnAction(e -> {
+ input.activeLayer = 1; input.activeTool = input.upperHeightTool;
+ root.setRight(toolPanel);
+ showToolParameters(toolPanel, input.activeTool);
+ });
+ holesBtn.setOnAction(e -> {
+ input.activeLayer = 2; input.activeTool = input.holeTool;
+ root.setRight(toolPanel);
+ showToolParameters(toolPanel, input.activeTool);
+ });
+ grassBtn.setOnAction(e -> {
+ input.activeLayer = 3; input.activeTool = input.grassTool;
+ root.setRight(toolPanel);
+ showToolParameters(toolPanel, input.activeTool);
+ });
+ textureBtn.setOnAction(e -> {
+ input.activeLayer = 4; input.activeTool = input.textureTool;
+ root.setRight(toolPanel);
+ showToolParameters(toolPanel, input.activeTool);
+ });
+ objPlaceBtn.setOnAction(e -> {
+ input.activeLayer = SharedInput.LAYER_OBJECTS;
+ root.setRight(buildObjectPlacePanel());
+ });
+ objEditBtn.setOnAction(e -> {
+ input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT;
+ input.pendingModelPath = null;
+ root.setRight(buildObjectEditPanel());
+ });
- Separator sep2 = new Separator(Orientation.VERTICAL);
+ CheckBox visibleCB = new CheckBox("Obere Schicht sichtbar");
+ visibleCB.setSelected(true);
+ visibleCB.setOnAction(e -> input.upperLayerVisible = visibleCB.isSelected());
- Label hint = new Label("WASD/QE: Kamera | Mitte-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;");
- toolBar.getItems().addAll(heightTool, sep1, brushLabel, brushSlider, sep2, hint);
+ toolBar.getItems().addAll(baseBtn, upperBtn, holesBtn, grassBtn, textureBtn,
+ new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn,
+ new Separator(Orientation.VERTICAL), visibleCB,
+ new Separator(Orientation.VERTICAL), hint);
- VBox top = new VBox(menuBar, toolBar);
- return top;
+ worldToolBar = toolBar;
+ return new VBox(menuBar, toolBar);
}
- // ── Linke Seite: Asset-Panel ─────────────────────────────────────────────
+ // ── Baum-Generator – Toolbar (ersetzt die Welt-Toolbar) ──────────────────
+
+ private ToolBar buildTreeToolBar() {
+ Label presetLabel = new Label("Baumart:");
+ presetLabel.setStyle("-fx-font-weight: bold;");
+ ComboBox presetBox = new ComboBox<>();
+ presetBox.getItems().addAll("Eiche", "Birke", "Kiefer", "Weide", "Busch");
+ presetBox.setValue("Eiche");
+ presetBox.setOnAction(e -> {
+ treeParams = presetFromName(presetBox.getValue());
+ root.setRight(buildTreeParamsPanel());
+ });
+
+ Label nameLabel = new Label("Export-Name:");
+ nameLabel.setStyle("-fx-font-weight: bold;");
+ treeNameField.setPrefWidth(130);
+
+ Button previewBtn = new Button("▶ Vorschau");
+ previewBtn.setStyle("-fx-font-weight: bold;");
+ previewBtn.setOnAction(e -> {
+ input.treeGenQueue.offer(
+ new SharedInput.TreeGenRequest(treeParams.copy(), false, treeNameField.getText().trim()));
+ setStatus("Generiere Vorschau…");
+ });
+
+ Button exportBtn = new Button("💾 Exportieren als .j3o");
+ exportBtn.setOnAction(e -> {
+ input.treeGenQueue.offer(
+ new SharedInput.TreeGenRequest(treeParams.copy(), true, treeNameField.getText().trim()));
+ setStatus("Generiere und exportiere…");
+ });
+
+ ToolBar bar = new ToolBar();
+ bar.getItems().addAll(
+ presetLabel, presetBox,
+ new Separator(Orientation.VERTICAL),
+ nameLabel, treeNameField,
+ previewBtn, exportBtn
+ );
+ return bar;
+ }
+
+ // ── Baum-Generator – zentrales Vorschau-Panel (ersetzt den Welt-Viewport) ──
+
+ private StackPane buildTreePreviewPanel() {
+ treePreviewView = new ImageView(input.treePreviewImage);
+ treePreviewView.setPreserveRatio(true);
+ treePreviewView.setSmooth(true);
+
+ StackPane previewPane = new StackPane(treePreviewView);
+ previewPane.setStyle("-fx-background-color: #1e2433;");
+ previewPane.widthProperty().addListener((o, ow, nw) -> {
+ treePreviewView.setFitWidth(nw.doubleValue());
+ input.treePreviewW = Math.max(64, nw.intValue());
+ });
+ previewPane.heightProperty().addListener((o, oh, nh) -> {
+ treePreviewView.setFitHeight(nh.doubleValue());
+ input.treePreviewH = Math.max(64, nh.intValue());
+ });
+
+ double[] prevMX = {0}, prevMY = {0};
+ previewPane.setOnMousePressed(e -> {
+ boolean mid = e.getButton() == MouseButton.MIDDLE;
+ boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
+ if (mid || both) { prevMX[0] = e.getX(); prevMY[0] = e.getY(); }
+ });
+ previewPane.setOnMouseDragged(e -> {
+ boolean both = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
+ if (e.isMiddleButtonDown() || both) {
+ double dx = e.getX() - prevMX[0];
+ double dy = e.getY() - prevMY[0];
+ input.treePreviewRotY += (float)(dx * 0.4);
+ input.treePreviewRotX = (float) Math.max(5.0, Math.min(80.0,
+ input.treePreviewRotX - dy * 0.3));
+ prevMX[0] = e.getX(); prevMY[0] = e.getY();
+ }
+ });
+ previewPane.setOnScroll(e -> {
+ float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f;
+ input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0,
+ input.treePreviewZoom * factor));
+ });
+
+ return previewPane;
+ }
+
+ // ── Baum-Generator – rechtes Parameter-Panel ─────────────────────────────
+
+ private VBox buildTreeParamsPanel() {
+ VBox inner = new VBox(8);
+ inner.setPadding(new Insets(10));
+
+ Label title = new Label("Baum-Parameter");
+ title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;");
+ inner.getChildren().addAll(title, new Separator());
+
+ // Zufallssamen
+ inner.getChildren().add(bold("Zufallssamen:"));
+ Spinner seedSpinner = new Spinner<>(0, 99999, treeParams.seed);
+ seedSpinner.setEditable(true);
+ seedSpinner.setMaxWidth(Double.MAX_VALUE);
+ seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b);
+ inner.getChildren().add(seedSpinner);
+
+ // Ast-Tiefe
+ inner.getChildren().add(bold("Ast-Tiefe:"));
+ Spinner levelSpinner = new Spinner<>(1, 4, treeParams.levels);
+ levelSpinner.setMaxWidth(Double.MAX_VALUE);
+ levelSpinner.valueProperty().addListener((o, a, b) -> treeParams.levels = b);
+ inner.getChildren().add(levelSpinner);
+
+ // Gravitation
+ inner.getChildren().add(paramSlider("Gravitation:", -0.15, 0.20, treeParams.gravityStrength,
+ v -> treeParams.gravityStrength = v));
+
+ inner.getChildren().add(new Separator());
+
+ // Blätter
+ CheckBox leavesCheck = new CheckBox("Blätter generieren");
+ leavesCheck.setSelected(treeParams.generateLeaves);
+ leavesCheck.setOnAction(e -> treeParams.generateLeaves = leavesCheck.isSelected());
+ inner.getChildren().add(leavesCheck);
+
+ inner.getChildren().add(paramSlider("Blattgröße:", 0.3, 3.0, treeParams.leafScale,
+ v -> treeParams.leafScale = v));
+
+ inner.getChildren().add(bold("Blattanzahl:"));
+ Spinner leafCountSpinner = new Spinner<>(1, 15, treeParams.leafCount);
+ leafCountSpinner.setMaxWidth(Double.MAX_VALUE);
+ leafCountSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafCount = b);
+ inner.getChildren().add(leafCountSpinner);
+
+ inner.getChildren().add(bold("Ast-Verzweigungen:"));
+ Spinner leafBranchingsSpinner = new Spinner<>(0, 3, treeParams.leafBranchings);
+ leafBranchingsSpinner.setMaxWidth(Double.MAX_VALUE);
+ leafBranchingsSpinner.valueProperty().addListener((o, a, b) -> treeParams.leafBranchings = b);
+ inner.getChildren().add(leafBranchingsSpinner);
+
+ inner.getChildren().add(new Separator());
+
+ // Wind
+ inner.getChildren().add(bold("Wind:"));
+ inner.getChildren().add(paramSlider("Stamm:", 0.0, 0.5, treeParams.trunkFlexibility,
+ v -> treeParams.trunkFlexibility = v));
+ inner.getChildren().add(paramSlider("Spitzen:", 0.2, 1.0, treeParams.branchFlexibility,
+ v -> treeParams.branchFlexibility = v));
+
+ ScrollPane scroll = new ScrollPane(inner);
+ scroll.setFitToWidth(true);
+ scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
+
+ VBox panel = new VBox(scroll);
+ VBox.setVgrow(scroll, Priority.ALWAYS);
+ panel.setPrefWidth(260);
+ panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
+ return panel;
+ }
+
+ // ── Kleine Helpers für Baum-Parameter-Panel ──────────────────────────────
+
+ private static Label bold(String text) {
+ Label l = new Label(text);
+ l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
+ return l;
+ }
+
+ private boolean isObjectMode() {
+ return input.activeLayer == SharedInput.LAYER_OBJECTS
+ || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT;
+ }
+
+ private static Label styledHint(String text) {
+ Label l = new Label(text);
+ l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;");
+ return l;
+ }
+
+ private static VBox paramSlider(String label, double min, double max, double init,
+ Consumer setter) {
+ Label name = new Label(label);
+ name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
+ Slider slider = new Slider(min, max, init);
+ slider.setShowTickMarks(true);
+ slider.setShowTickLabels(true);
+ slider.setMajorTickUnit((max - min) / 2.0);
+ slider.setMaxWidth(Double.MAX_VALUE);
+ Label val = new Label(String.format("%.3f", init));
+ val.setStyle("-fx-text-fill: #444; -fx-font-size: 11;");
+ slider.valueProperty().addListener((o, a, b) -> {
+ setter.accept(b.floatValue());
+ val.setText(String.format("%.3f", b.doubleValue()));
+ });
+ return new VBox(3, name, slider, val);
+ }
+
+ // ── EZ-Tree – Toolbar ────────────────────────────────────────────────────
+
+ private ToolBar buildEzTreeToolBar() {
+ Label presetLabel = new Label("Preset:");
+ presetLabel.setStyle("-fx-font-weight: bold;");
+ ComboBox presetBox = new ComboBox<>();
+ presetBox.getItems().addAll(TreePresets.presetNames());
+ presetBox.setValue("Oak Medium");
+ presetBox.setOnAction(e -> {
+ ezTreeOptions = TreePresets.byName(presetBox.getValue());
+ root.setRight(buildEzTreeParamsPanel());
+ });
+
+ Label nameLabel = new Label("Export-Name:");
+ nameLabel.setStyle("-fx-font-weight: bold;");
+ ezTreeNameField.setPrefWidth(130);
+
+ Button previewBtn = new Button("▶ Vorschau");
+ previewBtn.setStyle("-fx-font-weight: bold;");
+ previewBtn.setOnAction(e -> {
+ input.ezTreeGenQueue.offer(
+ new SharedInput.EzTreeGenRequest(
+ ezTreeOptions.copy(), false, ezTreeNameField.getText().trim()));
+ setStatus("EZ-Tree: generiere Vorschau…");
+ });
+
+ Button exportBtn = new Button("💾 Export .j3o");
+ exportBtn.setOnAction(e -> {
+ input.ezTreeGenQueue.offer(
+ new SharedInput.EzTreeGenRequest(
+ ezTreeOptions.copy(), true, ezTreeNameField.getText().trim()));
+ setStatus("EZ-Tree: generiere und exportiere…");
+ });
+
+ ToolBar bar = new ToolBar();
+ bar.getItems().addAll(
+ presetLabel, presetBox,
+ new Separator(Orientation.VERTICAL),
+ nameLabel, ezTreeNameField,
+ previewBtn, exportBtn
+ );
+ return bar;
+ }
+
+ // ── EZ-Tree – Parameter-Panel (alle Optionen) ────────────────────────────
+
+ private VBox buildEzTreeParamsPanel() {
+ VBox inner = new VBox(6);
+ inner.setPadding(new Insets(10));
+
+ // ── Allgemein ────────────────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
+
+ inner.getChildren().add(bold("Zufallssamen:"));
+ Spinner seedSp = intSpinner(0, 999999, ezTreeOptions.seed);
+ seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b);
+ inner.getChildren().add(seedSp);
+
+ inner.getChildren().add(bold("Typ:"));
+ ChoiceBox typeCB = new ChoiceBox<>();
+ typeCB.getItems().addAll("Laubbaum", "Immergrün");
+ typeCB.setValue(ezTreeOptions.type == TreeType.EVERGREEN ? "Immergrün" : "Laubbaum");
+ typeCB.setMaxWidth(Double.MAX_VALUE);
+ typeCB.setOnAction(e -> ezTreeOptions.type =
+ "Immergrün".equals(typeCB.getValue()) ? TreeType.EVERGREEN : TreeType.DECIDUOUS);
+ inner.getChildren().add(typeCB);
+
+ inner.getChildren().add(bold("Ast-Ebenen:"));
+ Spinner levelsSp = intSpinner(1, 5, ezTreeOptions.branch.levels);
+ levelsSp.valueProperty().addListener((o, a, b) -> {
+ ezTreeOptions.branch.levels = b;
+ root.setRight(buildEzTreeParamsPanel()); // rebuild für neue Level-Sektionen
+ });
+ inner.getChildren().add(levelsSp);
+
+ // ── Rinde ────────────────────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Rinde"), new Separator());
+
+ inner.getChildren().add(bold("Textur:"));
+ String[] barkTexNames = {"Keine", "Bark001", "Bark002", "Bark003", "Bark008"};
+ String[] barkTexPaths = {null,
+ "Textures/bark/Bark001_Color.jpg", "Textures/bark/Bark002_Color.jpg",
+ "Textures/bark/Bark003_Color.jpg", "Textures/bark/Bark008_Color.jpg"};
+ ChoiceBox barkTexCB = new ChoiceBox<>();
+ barkTexCB.getItems().addAll(barkTexNames);
+ barkTexCB.setValue(pathToName(ezTreeOptions.bark.textureFile, barkTexPaths, barkTexNames));
+ barkTexCB.setMaxWidth(Double.MAX_VALUE);
+ barkTexCB.setOnAction(e -> {
+ int idx = barkTexCB.getSelectionModel().getSelectedIndex();
+ ezTreeOptions.bark.textureFile = barkTexPaths[idx];
+ });
+ inner.getChildren().add(barkTexCB);
+
+ inner.getChildren().add(ezFloat("UV-Skala X:", 0.1, 5.0, ezTreeOptions.bark.textureScaleX,
+ v -> ezTreeOptions.bark.textureScaleX = v));
+ inner.getChildren().add(ezFloat("UV-Skala Y:", 0.1, 5.0, ezTreeOptions.bark.textureScaleY,
+ v -> ezTreeOptions.bark.textureScaleY = v));
+ inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.bark.r,
+ v -> ezTreeOptions.bark.r = v));
+ inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.bark.g,
+ v -> ezTreeOptions.bark.g = v));
+ inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.bark.b,
+ v -> ezTreeOptions.bark.b = v));
+
+ // ── Wachstumskraft ───────────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Wachstumskraft"), new Separator());
+ inner.getChildren().add(ezFloat("Richtung X:", -1, 1, ezTreeOptions.branch.force.direction.x,
+ v -> ezTreeOptions.branch.force.direction.x = v));
+ inner.getChildren().add(ezFloat("Richtung Y:", -1, 1, ezTreeOptions.branch.force.direction.y,
+ v -> ezTreeOptions.branch.force.direction.y = v));
+ inner.getChildren().add(ezFloat("Richtung Z:", -1, 1, ezTreeOptions.branch.force.direction.z,
+ v -> ezTreeOptions.branch.force.direction.z = v));
+ inner.getChildren().add(ezFloat("Stärke:", 0, 0.2, ezTreeOptions.branch.force.strength,
+ v -> ezTreeOptions.branch.force.strength = v));
+
+ // ── Pro-Ebene-Sektionen ──────────────────────────────────────────────
+ int maxLevel = ezTreeOptions.branch.levels;
+ for (int lv = 0; lv <= maxLevel; lv++) {
+ buildLevelSection(inner, lv, maxLevel);
+ }
+
+ // ── Blätter ──────────────────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
+
+ inner.getChildren().add(bold("Textur:"));
+ String[] leafTexNames = {"Keine", "Eiche", "Esche", "Espe", "Kiefer", "Palme"};
+ String[] leafTexPaths = {null,
+ "Textures/leaves/oak.png", "Textures/leaves/ash.png",
+ "Textures/leaves/aspen.png", "Textures/leaves/pine.png",
+ "Textures/leaves/palm.png"};
+ ChoiceBox leafTexCB = new ChoiceBox<>();
+ leafTexCB.getItems().addAll(leafTexNames);
+ leafTexCB.setValue(pathToName(ezTreeOptions.leaves.textureFile, leafTexPaths, leafTexNames));
+ leafTexCB.setMaxWidth(Double.MAX_VALUE);
+ leafTexCB.setOnAction(e -> {
+ int idx = leafTexCB.getSelectionModel().getSelectedIndex();
+ ezTreeOptions.leaves.textureFile = leafTexPaths[idx];
+ });
+ inner.getChildren().add(leafTexCB);
+
+ inner.getChildren().add(bold("Billboard:"));
+ ChoiceBox billCB = new ChoiceBox<>();
+ billCB.getItems().addAll("Kein", "Kreuz", "X-Drehung");
+ billCB.setValue(switch (ezTreeOptions.leaves.billboard) {
+ case CROSS -> "Kreuz";
+ case ROTATE_X -> "X-Drehung";
+ default -> "Kein";
+ });
+ billCB.setMaxWidth(Double.MAX_VALUE);
+ billCB.setOnAction(e -> ezTreeOptions.leaves.billboard = switch (billCB.getValue()) {
+ case "Kreuz" -> Billboard.CROSS;
+ case "X-Drehung" -> Billboard.ROTATE_X;
+ default -> Billboard.NONE;
+ });
+ inner.getChildren().add(billCB);
+
+ inner.getChildren().add(bold("Anzahl:"));
+ Spinner leafCountSp = intSpinner(0, 60, ezTreeOptions.leaves.count);
+ leafCountSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.leaves.count = b);
+ inner.getChildren().add(leafCountSp);
+
+ inner.getChildren().add(ezFloat("Start-Position:", 0, 1, ezTreeOptions.leaves.start,
+ v -> ezTreeOptions.leaves.start = v));
+ inner.getChildren().add(ezFloat("Größe:", 0.05, 4, ezTreeOptions.leaves.size,
+ v -> ezTreeOptions.leaves.size = v));
+ inner.getChildren().add(ezFloat("Größen-Varianz:", 0, 1, ezTreeOptions.leaves.sizeVariance,
+ v -> ezTreeOptions.leaves.sizeVariance = v));
+ inner.getChildren().add(ezFloat("Alpha-Schwellwert:", 0, 1, ezTreeOptions.leaves.alphaTest,
+ v -> ezTreeOptions.leaves.alphaTest = v));
+ inner.getChildren().add(ezFloat("Farbe R:", 0, 1, ezTreeOptions.leaves.r,
+ v -> ezTreeOptions.leaves.r = v));
+ inner.getChildren().add(ezFloat("Farbe G:", 0, 1, ezTreeOptions.leaves.g,
+ v -> ezTreeOptions.leaves.g = v));
+ inner.getChildren().add(ezFloat("Farbe B:", 0, 1, ezTreeOptions.leaves.b,
+ v -> ezTreeOptions.leaves.b = v));
+
+ // ── Spalier ──────────────────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Spalier (Trellis)"), new Separator());
+
+ CheckBox trellisEnCB = new CheckBox("Aktiviert");
+ trellisEnCB.setSelected(ezTreeOptions.trellis.enabled);
+ trellisEnCB.setOnAction(e -> ezTreeOptions.trellis.enabled = trellisEnCB.isSelected());
+ inner.getChildren().add(trellisEnCB);
+
+ inner.getChildren().add(bold("Streben:"));
+ Spinner trellisSecSp = intSpinner(3, 12, ezTreeOptions.trellis.sections);
+ trellisSecSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.sections = b);
+ inner.getChildren().add(trellisSecSp);
+
+ inner.getChildren().add(ezFloat("Radius:", 0.1, 8, ezTreeOptions.trellis.radius,
+ v -> ezTreeOptions.trellis.radius = v));
+ inner.getChildren().add(ezFloat("Höhe:", 0.5, 15, ezTreeOptions.trellis.length,
+ v -> ezTreeOptions.trellis.length = v));
+ inner.getChildren().add(ezFloat("Streben-Radius:", 0.01, 0.3, ezTreeOptions.trellis.memberRadius,
+ v -> ezTreeOptions.trellis.memberRadius = v));
+
+ inner.getChildren().add(bold("Querstreben:"));
+ Spinner crossSp = intSpinner(1, 8, ezTreeOptions.trellis.crossMembers);
+ crossSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.trellis.crossMembers = b);
+ inner.getChildren().add(crossSp);
+
+ ScrollPane scroll = new ScrollPane(inner);
+ scroll.setFitToWidth(true);
+ scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
+
+ VBox panel = new VBox(scroll);
+ VBox.setVgrow(scroll, Priority.ALWAYS);
+ panel.setPrefWidth(270);
+ panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
+ return panel;
+ }
+
+ // ── Palmen-Generator – Toolbar ───────────────────────────────────────────
+
+ private ToolBar buildPalmToolBar() {
+ Label nameLabel = new Label("Export-Name:");
+ nameLabel.setStyle("-fx-font-weight: bold;");
+ palmNameField.setPrefWidth(130);
+
+ Button previewBtn = new Button("▶ Vorschau");
+ previewBtn.setStyle("-fx-font-weight: bold;");
+ previewBtn.setOnAction(e -> {
+ input.palmGenQueue.offer(
+ new SharedInput.PalmGenRequest(
+ palmOptions.copy(), false, palmNameField.getText().trim()));
+ setStatus("Palme: generiere Vorschau…");
+ });
+
+ Button exportBtn = new Button("💾 Export .j3o");
+ exportBtn.setOnAction(e -> {
+ input.palmGenQueue.offer(
+ new SharedInput.PalmGenRequest(
+ palmOptions.copy(), true, palmNameField.getText().trim()));
+ setStatus("Palme: generiere und exportiere…");
+ });
+
+ ToolBar bar = new ToolBar();
+ bar.getItems().addAll(
+ nameLabel, palmNameField,
+ previewBtn, exportBtn
+ );
+ return bar;
+ }
+
+ // ── Palmen-Generator – Parameter-Panel ──────────────────────────────────
+
+ private VBox buildPalmParamsPanel() {
+ VBox inner = new VBox(6);
+ inner.setPadding(new Insets(10));
+
+ inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
+ inner.getChildren().add(bold("Zufallssamen:"));
+ Spinner seedSp = intSpinner(0, 999999, palmOptions.seed);
+ seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b);
+ inner.getChildren().add(seedSp);
+
+ inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
+ inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight,
+ v -> palmOptions.trunkHeight = v));
+ inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom,
+ v -> palmOptions.trunkRadiusBottom = v));
+ inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop,
+ v -> palmOptions.trunkRadiusTop = v));
+
+ inner.getChildren().addAll(sectionTitle("Wedel"), new Separator());
+ inner.getChildren().add(bold("Anzahl:"));
+ Spinner frondCountSp = intSpinner(3, 20, palmOptions.frondCount);
+ frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b);
+ inner.getChildren().add(frondCountSp);
+ inner.getChildren().add(ezFloat("Winkel min (° von oben):", 60, 95, palmOptions.frondAngleMin,
+ v -> palmOptions.frondAngleMin = v));
+ inner.getChildren().add(ezFloat("Winkel max (° von oben):", 85, 130, palmOptions.frondAngleMax,
+ v -> palmOptions.frondAngleMax = v));
+ inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength,
+ v -> palmOptions.frondLength = v));
+
+ inner.getChildren().addAll(sectionTitle("Blätter (Fiederblättchen)"), new Separator());
+ inner.getChildren().add(bold("Paare pro Wedel:"));
+ Spinner pairsSp = intSpinner(3, 16, palmOptions.frondLeafletPairs);
+ pairsSp.valueProperty().addListener((o, a, b) -> palmOptions.frondLeafletPairs = b);
+ inner.getChildren().add(pairsSp);
+ inner.getChildren().add(ezFloat("Wedel-Breite:", 0.2, 4.0, palmOptions.frondWidth,
+ v -> palmOptions.frondWidth = v));
+
+ inner.getChildren().addAll(sectionTitle("Farben"), new Separator());
+ inner.getChildren().add(ezFloat("Stamm R:", 0, 1, palmOptions.barkR, v -> palmOptions.barkR = v));
+ inner.getChildren().add(ezFloat("Stamm G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v));
+ inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = v));
+ inner.getChildren().add(ezFloat("Blatt R:", 0, 1, palmOptions.leafR, v -> palmOptions.leafR = v));
+ inner.getChildren().add(ezFloat("Blatt G:", 0, 1, palmOptions.leafG, v -> palmOptions.leafG = v));
+ inner.getChildren().add(ezFloat("Blatt B:", 0, 1, palmOptions.leafB, v -> palmOptions.leafB = v));
+
+ ScrollPane scroll = new ScrollPane(inner);
+ scroll.setFitToWidth(true);
+ scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
+
+ VBox panel = new VBox(scroll);
+ VBox.setVgrow(scroll, Priority.ALWAYS);
+ panel.setPrefWidth(270);
+ panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
+ return panel;
+ }
+
+ private void buildLevelSection(VBox container, int lv, int maxLevel) {
+ String title = lv == 0 ? "Stamm (Ebene 0)" : "Ast-Ebene " + lv;
+ container.getChildren().addAll(sectionTitle(title), new Separator());
+
+ var bo = ezTreeOptions.branch;
+
+ // Winkel + Start nur ab Ebene 1 (da Kind-Eigenschaften)
+ if (lv > 0) {
+ container.getChildren().add(ezFloat("Winkel (°):", 0, 90,
+ bo.getAngle(lv), v -> bo.angle.put(lv, v)));
+ container.getChildren().add(ezFloat("Start-Position:", 0, 1,
+ bo.getStart(lv), v -> bo.start.put(lv, v)));
+ }
+
+ // Kinder nur wenn noch weitere Ebenen folgen
+ if (lv < maxLevel) {
+ container.getChildren().add(bold("Kinder:"));
+ Spinner childSp = intSpinner(0, 20, bo.getChildren(lv));
+ childSp.valueProperty().addListener((o, a, b) -> bo.children.put(lv, b));
+ container.getChildren().add(childSp);
+ }
+
+ container.getChildren().add(ezFloat("Gnarliness:", 0, 1.5,
+ bo.getGnarliness(lv), v -> bo.gnarliness.put(lv, v)));
+
+ String lenLabel = lv == 0 ? "Länge (absolut):" : "Länge (Faktor):";
+ double lenMax = lv == 0 ? 60 : 2;
+ container.getChildren().add(ezFloat(lenLabel, 0.1, lenMax,
+ bo.getLength(lv), v -> bo.length.put(lv, v)));
+
+ String radLabel = lv == 0 ? "Radius (absolut):" : "Radius (Faktor):";
+ double radMax = lv == 0 ? 3 : 1;
+ container.getChildren().add(ezFloat(radLabel, 0.01, radMax,
+ bo.getRadius(lv), v -> bo.radius.put(lv, v)));
+
+ container.getChildren().add(bold("Sektionen:"));
+ Spinner secSp = intSpinner(2, 24, bo.getSections(lv));
+ secSp.valueProperty().addListener((o, a, b) -> bo.sections.put(lv, b));
+ container.getChildren().add(secSp);
+
+ container.getChildren().add(bold("Segmente (Polygon):"));
+ Spinner segSp = intSpinner(3, 12, bo.getSegments(lv));
+ segSp.valueProperty().addListener((o, a, b) -> bo.segments.put(lv, b));
+ container.getChildren().add(segSp);
+
+ container.getChildren().add(ezFloat("Verjüngung:", 0, 1,
+ bo.getTaper(lv), v -> bo.taper.put(lv, v)));
+ container.getChildren().add(ezFloat("Drall (°/Sektion):", -180, 180,
+ bo.getTwist(lv), v -> bo.twist.put(lv, v)));
+ }
+
+ // ── EZ-Tree UI-Helpers ───────────────────────────────────────────────────
+
+ private static Label sectionTitle(String text) {
+ Label l = new Label(text);
+ l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;");
+ VBox.setMargin(l, new Insets(8, 0, 0, 0));
+ return l;
+ }
+
+ private static Spinner intSpinner(int min, int max, int init) {
+ Spinner sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init)));
+ sp.setEditable(true);
+ sp.setMaxWidth(Double.MAX_VALUE);
+ return sp;
+ }
+
+ private static VBox ezFloat(String label, double min, double max, double init,
+ java.util.function.Consumer setter) {
+ Label name = new Label(label);
+ name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
+ Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init)));
+ slider.setShowTickMarks(false);
+ slider.setMaxWidth(Double.MAX_VALUE);
+ Label val = new Label(String.format("%.3f", init));
+ val.setStyle("-fx-text-fill: #444; -fx-font-size: 11;");
+ slider.valueProperty().addListener((o, a, b) -> {
+ setter.accept(b.floatValue());
+ val.setText(String.format("%.3f", b.doubleValue()));
+ });
+ HBox row = new HBox(6, slider, val);
+ HBox.setHgrow(slider, Priority.ALWAYS);
+ return new VBox(2, name, row);
+ }
+
+ private static String pathToName(String path, String[] paths, String[] names) {
+ if (path == null) return names[0];
+ for (int i = 0; i < paths.length; i++) {
+ if (path.equals(paths[i])) return names[i];
+ }
+ return names[0];
+ }
+
+ private static TreeParams presetFromName(String name) {
+ return switch (name) {
+ case "Birke" -> TreeParams.birch();
+ case "Kiefer" -> TreeParams.pine();
+ case "Weide" -> TreeParams.willow();
+ case "Busch" -> TreeParams.bush();
+ default -> TreeParams.oak();
+ };
+ }
+
+ // ── Objekt-Werkzeug – Platzieren-Panel ──────────────────────────────────
+
+ private VBox buildObjectPlacePanel() {
+ VBox inner = new VBox(8);
+ inner.setPadding(new Insets(10));
+
+ // ── Modell aus Asset-Baum ─────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Modell"), new Separator());
+ objModelLabel = new Label(input.pendingModelPath != null
+ && !input.pendingModelPath.startsWith("@")
+ ? input.pendingModelPath : "(Doppelklick im Asset-Baum)");
+ objModelLabel.setWrapText(true);
+ objModelLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;");
+ inner.getChildren().add(objModelLabel);
+ inner.getChildren().add(styledHint("Doppelklick auf Modell links → auswählen"));
+ inner.getChildren().add(styledHint("Linksklick ins Terrain → platzieren"));
+
+ // ── Platziermodus ─────────────────────────────────────────────────────
+ boolean prevMulti = multiPlaceCB != null && multiPlaceCB.isSelected();
+ multiPlaceCB = new CheckBox("Mehrfach platzieren");
+ multiPlaceCB.setSelected(prevMulti);
+ multiPlaceCB.setStyle("-fx-text-fill: #111111;");
+ multiPlaceCB.setTooltip(new Tooltip(
+ "Aktiv: Objekte nacheinander platzieren.\n" +
+ "Inaktiv: Nach dem Platzieren automatisch in den Bearbeiten-Modus wechseln."));
+ inner.getChildren().add(multiPlaceCB);
+
+ // ── Meshes (Primitive) ────────────────────────────────────────────────
+ inner.getChildren().addAll(sectionTitle("Meshes"), new Separator());
+
+ meshToggleGroup = new ToggleGroup();
+ String[][] shapes = {
+ {"Box", "@box"},
+ {"Kugel", "@sphere"},
+ {"Zylinder", "@cylinder"},
+ {"Ebene", "@plane"},
+ };
+
+ // 2-spaltige Gitteranordnung
+ javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane();
+ grid.setHgap(6);
+ grid.setVgap(6);
+ grid.setMaxWidth(Double.MAX_VALUE);
+
+ for (int i = 0; i < shapes.length; i++) {
+ String label = shapes[i][0];
+ String marker = shapes[i][1];
+ ToggleButton btn = new ToggleButton(label);
+ btn.setToggleGroup(meshToggleGroup);
+ btn.setMaxWidth(Double.MAX_VALUE);
+ btn.setStyle("-fx-font-size: 12;");
+ btn.setOnAction(e -> {
+ if (btn.isSelected()) {
+ input.pendingModelPath = marker;
+ if (objModelLabel != null)
+ objModelLabel.setText("(Doppelklick im Asset-Baum)");
+ setStatus("Mesh: " + label + " | Linksklick ins Terrain zum Platzieren");
+ } else {
+ input.pendingModelPath = null;
+ }
+ });
+ // Vorauswahl wiederherstellen
+ if (marker.equals(input.pendingModelPath)) btn.setSelected(true);
+ grid.add(btn, i % 2, i / 2);
+ javafx.scene.layout.GridPane.setHgrow(btn, Priority.ALWAYS);
+ }
+ inner.getChildren().add(grid);
+
+ ScrollPane scroll = new ScrollPane(inner);
+ scroll.setFitToWidth(true);
+ scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
+
+ VBox panel = new VBox(scroll);
+ VBox.setVgrow(scroll, Priority.ALWAYS);
+ panel.setPrefWidth(260);
+ panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
+ return panel;
+ }
+
+ // ── Objekt-Werkzeug – Bearbeiten-Panel ──────────────────────────────────
+
+ private VBox buildObjectEditPanel() {
+ VBox inner = new VBox(8);
+ inner.setPadding(new Insets(10));
+
+ inner.getChildren().addAll(sectionTitle("Gewähltes Objekt"), new Separator());
+
+ objSolidCB = new CheckBox("Solid (Charakter-Kollision)");
+ objSolidCB.setDisable(true);
+ objSolidCB.setOnAction(e -> input.pendingSolidChange = objSolidCB.isSelected());
+ inner.getChildren().add(objSolidCB);
+
+ objPosLabel = new Label("Position: –");
+ objPosLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
+ objPosLabel.setWrapText(true);
+ inner.getChildren().add(objPosLabel);
+
+ inner.getChildren().add(new Separator());
+ inner.getChildren().add(styledHint("Linksklick auf Objekt → auswählen"));
+ inner.getChildren().add(styledHint("Pfeile ziehen → X/Y/Z bewegen"));
+ inner.getChildren().add(styledHint("Ring ziehen → Y-Achse drehen"));
+
+ ScrollPane scroll = new ScrollPane(inner);
+ scroll.setFitToWidth(true);
+ scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
+
+ VBox panel = new VBox(scroll);
+ VBox.setVgrow(scroll, Priority.ALWAYS);
+ panel.setPrefWidth(260);
+ panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
+ return panel;
+ }
+
+ private void updateObjectPanel(String info) {
+ if (objSolidCB == null || objPosLabel == null) return;
+ if (info == null) {
+ objSolidCB.setDisable(true);
+ objSolidCB.setSelected(false);
+ objPosLabel.setText("Position: –");
+ return;
+ }
+ // info format: "modelPath|solid|x|y|z|rotY|scale"
+ String[] parts = info.split("\\|", 7);
+ if (parts.length < 7) return;
+ objSolidCB.setDisable(false);
+ objSolidCB.setSelected(Boolean.parseBoolean(parts[1]));
+ try {
+ float x = Float.parseFloat(parts[2]);
+ float y = Float.parseFloat(parts[3]);
+ float z = Float.parseFloat(parts[4]);
+ float rotY = (float) Math.toDegrees(Float.parseFloat(parts[5]));
+ objPosLabel.setText(String.format(
+ "X: %.1f Y: %.1f Z: %.1f%nRot Y: %.1f°", x, y, z, rotY));
+ } catch (NumberFormatException ignored) {}
+ }
+
+ // ── Rechte Seite: Tool-Parameter-Panel (Welteneditor) ────────────────────
+
+ private VBox buildToolPanel() {
+ VBox panel = new VBox(10);
+ panel.setPadding(new Insets(10));
+ panel.setPrefWidth(260);
+ panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
+ showToolParameters(panel, input.activeTool);
+ return panel;
+ }
+
+ private void showToolParameters(VBox panel, EditorTool tool) {
+ panel.getChildren().clear();
+
+ Label toolTitle = new Label(tool.getName());
+ toolTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;");
+ panel.getChildren().addAll(toolTitle, new Separator());
+
+ for (ChoiceToolParameter param : tool.getChoiceParameters()) {
+ Label nameLabel = new Label(param.getName());
+ nameLabel.setStyle("-fx-text-fill: #111111;");
+
+ if (param.getImagePaths() != null) {
+ String[] paths = param.getImagePaths();
+ String[] labels = param.getChoices();
+ ToggleGroup tg = new ToggleGroup();
+ ToggleButton[] buttons = new ToggleButton[paths.length];
+ HBox row = new HBox(3);
+
+ for (int j = 0; j < paths.length; j++) {
+ final int index = j;
+ ToggleButton btn = new ToggleButton();
+ btn.setToggleGroup(tg);
+ btn.setTooltip(new Tooltip(labels[j]));
+ btn.setMinSize(50, 50);
+ btn.setMaxSize(50, 50);
+
+ String imgUrlStr = null;
+ URL resUrl = EditorApp.class.getResource("/" + paths[j]);
+ if (resUrl != null) {
+ imgUrlStr = resUrl.toString();
+ } else {
+ File imgFile = new File(paths[j]).getAbsoluteFile();
+ if (imgFile.exists()) imgUrlStr = imgFile.toURI().toString();
+ }
+ if (imgUrlStr != null) {
+ Image img = new Image(imgUrlStr, 38, 38, true, true);
+ if (!img.isError()) btn.setGraphic(new ImageView(img));
+ else btn.setText(labels[j].substring(0, 2));
+ } else {
+ btn.setText(labels[j].substring(0, 2));
+ }
+
+ boolean initially = (j == param.getSelectedIndex());
+ btn.setSelected(initially);
+ applyModeButtonStyle(btn, initially);
+ btn.selectedProperty().addListener((obs, was, isNow) -> {
+ applyModeButtonStyle(btn, isNow);
+ if (isNow) param.setSelectedIndex(index);
+ });
+
+ buttons[j] = btn;
+ row.getChildren().add(btn);
+ }
+
+ tg.selectedToggleProperty().addListener((obs, oldT, newT) -> {
+ if (newT == null) tg.selectToggle(oldT);
+ });
+ panel.getChildren().addAll(nameLabel, row);
+ } else {
+ ChoiceBox choiceBox = new ChoiceBox<>();
+ choiceBox.getItems().addAll(param.getChoices());
+ choiceBox.getSelectionModel().select(param.getSelectedIndex());
+ choiceBox.setMaxWidth(Double.MAX_VALUE);
+ choiceBox.getSelectionModel().selectedIndexProperty().addListener(
+ (obs, oldV, newV) -> param.setSelectedIndex(newV.intValue()));
+ panel.getChildren().addAll(nameLabel, choiceBox);
+ }
+ }
+
+ for (ToolParameter param : tool.getParameters()) {
+ Label name = new Label(param.getName());
+ name.setStyle("-fx-text-fill: #111111;");
+
+ Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue());
+ slider.setShowTickMarks(true);
+ slider.setShowTickLabels(true);
+ slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2);
+ slider.setMaxWidth(Double.MAX_VALUE);
+
+ Label valueLabel = new Label(String.format("%.3f", param.getValue()));
+ valueLabel.setStyle("-fx-text-fill: #444; -fx-font-size: 11;");
+
+ slider.valueProperty().addListener((obs, oldV, newV) -> {
+ param.setValue(newV.doubleValue());
+ valueLabel.setText(String.format("%.3f", newV.doubleValue()));
+ });
+
+ panel.getChildren().add(new VBox(3, name, slider, valueLabel));
+ }
+ }
+
+ // ── Linke Seite: Asset-Panel (Welteneditor) ──────────────────────────────
private VBox buildAssetPanel() {
VBox panel = new VBox(6);
@@ -125,30 +1167,54 @@ public class EditorApp extends Application {
Label title = new Label("Assets");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
- // Baum
- TreeItem root = new TreeItem<>("Projekt");
- root.setExpanded(true);
+ TreeItem assetRoot = new TreeItem<>("Projekt");
+ assetRoot.setExpanded(true);
modelsNode = new TreeItem<>("Models");
texturesNode = new TreeItem<>("Texturen");
audioNode = new TreeItem<>("Audio");
- root.getChildren().addAll(modelsNode, texturesNode, audioNode);
+ assetRoot.getChildren().addAll(modelsNode, texturesNode, audioNode);
- // Bestehende Assets laden
- loadAssetsInto(modelsNode, "models", ".j3o", ".obj", ".fbx", ".gltf", ".glb");
- loadAssetsInto(texturesNode, "textures", ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds");
- loadAssetsInto(audioNode, "audio", ".ogg", ".wav", ".mp3");
+ itemPaths.put(modelsNode, ASSET_ROOT.resolve("models"));
+ itemPaths.put(texturesNode, ASSET_ROOT.resolve("textures"));
+ itemPaths.put(audioNode, ASSET_ROOT.resolve("audio"));
- TreeView tree = new TreeView<>(root);
+ loadAssetsRecursive(modelsNode, ASSET_ROOT.resolve("models"),
+ ".j3o", ".obj", ".fbx", ".gltf", ".glb");
+ loadAssetsRecursive(texturesNode, ASSET_ROOT.resolve("textures"),
+ ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds");
+ loadAssetsRecursive(audioNode, ASSET_ROOT.resolve("audio"),
+ ".ogg", ".wav", ".mp3");
+
+ TreeView tree = new TreeView<>(assetRoot);
tree.setShowRoot(false);
VBox.setVgrow(tree, Priority.ALWAYS);
+ tree.setCellFactory(tv -> buildAssetCell());
- // Kontextmenü im Baum (Datei öffnen im Dateimanager)
- ContextMenu ctx = new ContextMenu();
- MenuItem showItem = new MenuItem("Im Dateisystem anzeigen");
- ctx.getItems().add(showItem);
- tree.setContextMenu(ctx);
+ // F2 → Umbenennen
+ tree.setOnKeyPressed(e -> {
+ if (e.getCode() != KeyCode.F2) return;
+ TreeItem sel = tree.getSelectionModel().getSelectedItem();
+ if (sel == null || sel == modelsNode || sel == texturesNode || sel == audioNode) return;
+ renameAsset(sel);
+ e.consume();
+ });
+
+ // Doppelklick auf ein Modell → als aktives Objekt-Platzierungs-Modell wählen
+ tree.setOnMouseClicked(e -> {
+ if (e.getClickCount() != 2 || input.activeLayer != SharedInput.LAYER_OBJECTS) return;
+ TreeItem sel = tree.getSelectionModel().getSelectedItem();
+ if (sel == null || isAssetFolder(sel)) return;
+ if (getCategoryRoot(sel) != modelsNode) return;
+ Path p = itemPaths.get(sel);
+ if (p == null) return;
+ String path = ASSET_ROOT.relativize(p).toString().replace('\\', '/');
+ input.pendingModelPath = path;
+ // Mesh-Primitiv-Auswahl aufheben
+ if (meshToggleGroup != null) meshToggleGroup.selectToggle(null);
+ if (objModelLabel != null) objModelLabel.setText(path);
+ setStatus("Objekt-Modell: " + path + " | Linksklick ins Terrain zum Platzieren");
+ });
- // Import-Button
Button importBtn = new Button("⊕ Import…");
importBtn.setMaxWidth(Double.MAX_VALUE);
importBtn.setOnAction(e -> handleImport(tree.getScene().getWindow()));
@@ -157,19 +1223,324 @@ public class EditorApp extends Application {
return panel;
}
- private void loadAssetsInto(TreeItem parent, String subDir, String... exts) {
- Path dir = ASSET_ROOT.resolve(subDir);
+ // ── Asset-Tree-Cell (DnD + Kontextmenü) ──────────────────────────────────
+
+ private TreeCell buildAssetCell() {
+ TreeCell cell = new TreeCell<>() {
+ @Override
+ protected void updateItem(String item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) { setText(null); setStyle(""); return; }
+ setText(item);
+ setCellStyle(this, false);
+ }
+ };
+
+ // ── Drag-Quelle: nur Dateien (keine Ordner) ──────────────────────────
+ cell.setOnDragDetected(e -> {
+ TreeItem item = cell.getTreeItem();
+ if (item == null || isAssetFolder(item)) return;
+ draggedItem = item;
+ Dragboard db = cell.startDragAndDrop(TransferMode.MOVE);
+ ClipboardContent cc = new ClipboardContent();
+ cc.putString(item.getValue());
+ db.setContent(cc);
+ e.consume();
+ });
+
+ // ── Drag-Ziel: nur Ordner der gleichen Kategorie ─────────────────────
+ cell.setOnDragOver(e -> {
+ if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem()))
+ e.acceptTransferModes(TransferMode.MOVE);
+ e.consume();
+ });
+
+ cell.setOnDragEntered(e -> {
+ if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem()))
+ setCellStyle(cell, true);
+ e.consume();
+ });
+
+ cell.setOnDragExited(e -> { setCellStyle(cell, false); e.consume(); });
+
+ cell.setOnDragDropped(e -> {
+ boolean ok = false;
+ TreeItem target = cell.getTreeItem();
+ if (draggedItem != null && target != null && isValidDropTarget(target)) {
+ Path src = itemPaths.get(draggedItem);
+ Path destDir = itemPaths.get(target);
+ Path dest = destDir.resolve(src.getFileName());
+ if (!src.equals(dest)) {
+ if (Files.exists(dest)) {
+ setStatus("Fehler: Datei mit diesem Namen existiert bereits im Zielordner.");
+ } else {
+ try {
+ Files.move(src, dest);
+ draggedItem.getParent().getChildren().remove(draggedItem);
+ itemPaths.remove(draggedItem);
+ TreeItem moved = new TreeItem<>(src.getFileName().toString());
+ itemPaths.put(moved, dest);
+ target.getChildren().add(moved);
+ target.setExpanded(true);
+ setStatus("Verschoben: " + src.getFileName());
+ ok = true;
+ } catch (IOException ex) {
+ setStatus("Verschieben fehlgeschlagen: " + ex.getMessage());
+ }
+ }
+ }
+ }
+ draggedItem = null;
+ e.setDropCompleted(ok);
+ e.consume();
+ });
+
+ cell.setOnDragDone(e -> { draggedItem = null; e.consume(); });
+
+ // ── Kontextmenü ──────────────────────────────────────────────────────
+ cell.setOnContextMenuRequested(e -> {
+ TreeItem item = cell.getTreeItem();
+ if (item == null) return;
+ ContextMenu ctx = new ContextMenu();
+
+ if (isAssetFolder(item)) {
+ MenuItem newSub = new MenuItem("📁 Neuer Unterordner…");
+ newSub.setOnAction(ev -> createSubfolder(item));
+ ctx.getItems().add(newSub);
+
+ boolean isCatRoot = (item == modelsNode || item == texturesNode || item == audioNode);
+ Path dir = itemPaths.get(item);
+
+ if (!isCatRoot && dir != null) {
+ MenuItem rename = new MenuItem("✏ Umbenennen…");
+ rename.setOnAction(ev -> renameAsset(item));
+ ctx.getItems().add(rename);
+
+ // Ordner löschen nur wenn leer
+ boolean empty;
+ try (var s = Files.list(dir)) { empty = s.findFirst().isEmpty(); }
+ catch (IOException ex) { empty = false; }
+ MenuItem del = new MenuItem("🗑 Ordner löschen");
+ del.setDisable(!empty);
+ if (!empty) del.setStyle("-fx-text-fill: grey;");
+ del.setOnAction(ev -> {
+ try {
+ Files.deleteIfExists(dir);
+ item.getParent().getChildren().remove(item);
+ itemPaths.remove(item);
+ setStatus("Ordner gelöscht: " + dir.getFileName());
+ } catch (IOException ex) {
+ setStatus("Fehler: " + ex.getMessage());
+ }
+ });
+ ctx.getItems().addAll(new SeparatorMenuItem(), del);
+ }
+ } else {
+ // Datei: Umbenennen + Im Dateisystem anzeigen + Löschen
+ Path p = itemPaths.get(item);
+ if (p != null) {
+ MenuItem rename = new MenuItem("✏ Umbenennen…");
+ rename.setOnAction(ev -> renameAsset(item));
+ MenuItem reveal = new MenuItem("Im Dateisystem anzeigen");
+ reveal.setOnAction(ev -> {
+ try { Runtime.getRuntime().exec(
+ new String[]{"xdg-open", p.getParent().toString()});
+ } catch (IOException ignored) {}
+ });
+ MenuItem del = new MenuItem("🗑 Löschen");
+ del.setStyle("-fx-text-fill: #c0392b;");
+ del.setOnAction(ev -> {
+ Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
+ "Datei wirklich löschen?\n" + p.getFileName(),
+ ButtonType.OK, ButtonType.CANCEL);
+ confirm.setHeaderText(null);
+ confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
+ try {
+ Files.deleteIfExists(p);
+ item.getParent().getChildren().remove(item);
+ itemPaths.remove(item);
+ setStatus("Gelöscht: " + p.getFileName());
+ } catch (IOException ex) {
+ setStatus("Fehler: " + ex.getMessage());
+ }
+ });
+ });
+ ctx.getItems().addAll(rename, reveal, new SeparatorMenuItem(), del);
+ }
+ }
+
+ if (!ctx.getItems().isEmpty())
+ ctx.show(cell, e.getScreenX(), e.getScreenY());
+ e.consume();
+ });
+
+ return cell;
+ }
+
+ private void setCellStyle(TreeCell cell, boolean highlighted) {
+ String base = isAssetFolder(cell.getTreeItem()) ? "-fx-font-weight:bold;" : "";
+ cell.setStyle(highlighted ? base + "-fx-background-color:#cce5ff;" : base);
+ }
+
+ // ── Asset-Baum Hilfsmethoden ──────────────────────────────────────────────
+
+ /** Gibt true zurück wenn das Item ein Verzeichnis ist (inkl. Kategoriewurzeln). */
+ private boolean isAssetFolder(TreeItem item) {
+ if (item == null) return false;
+ Path p = itemPaths.get(item);
+ return p != null && Files.isDirectory(p);
+ }
+
+ /** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode). */
+ private TreeItem getCategoryRoot(TreeItem item) {
+ for (TreeItem cur = item; cur != null; cur = cur.getParent())
+ if (cur == modelsNode || cur == texturesNode || cur == audioNode) return cur;
+ return null;
+ }
+
+ /** Gibt true zurück wenn draggedItem auf dieses target verschoben werden darf. */
+ private boolean isValidDropTarget(TreeItem target) {
+ if (draggedItem == null || target == null) return false;
+ if (!isAssetFolder(target)) return false;
+ if (target == draggedItem.getParent()) return false; // schon dort
+ TreeItem dragCat = getCategoryRoot(draggedItem);
+ TreeItem dropCat = getCategoryRoot(target);
+ return dragCat != null && dragCat == dropCat;
+ }
+
+ /** Erstellt einen neuen Unterordner unter parentItem (Dialog + Filesystem). */
+ private void createSubfolder(TreeItem parentItem) {
+ Path parentPath = itemPaths.get(parentItem);
+ if (parentPath == null) return;
+ TextInputDialog dlg = new TextInputDialog("Neuer Ordner");
+ dlg.setTitle("Ordner erstellen");
+ dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:");
+ dlg.setContentText("Name:");
+ dlg.showAndWait().ifPresent(raw -> {
+ String name = raw.trim();
+ if (name.isEmpty()) return;
+ Path newDir = parentPath.resolve(name);
+ try {
+ Files.createDirectories(newDir);
+ TreeItem newItem = new TreeItem<>(name);
+ itemPaths.put(newItem, newDir);
+ parentItem.getChildren().add(0, newItem); // Ordner an erster Stelle
+ parentItem.setExpanded(true);
+ setStatus("Ordner erstellt: " + name);
+ } catch (IOException ex) {
+ setStatus("Fehler beim Erstellen: " + ex.getMessage());
+ }
+ });
+ }
+
+ /** Benennt eine Datei oder einen Ordner um (Filesystem + TreeItem + itemPaths). */
+ private void renameAsset(TreeItem item) {
+ Path oldPath = itemPaths.get(item);
+ if (oldPath == null) return;
+
+ boolean isFile = !Files.isDirectory(oldPath);
+ String oldFileName = oldPath.getFileName().toString();
+
+ // For files: extract extension and pre-fill dialog with base name only
+ String ext = "";
+ String baseName = oldFileName;
+ if (isFile) {
+ int dot = oldFileName.lastIndexOf('.');
+ if (dot > 0) {
+ ext = oldFileName.substring(dot); // e.g. ".j3o"
+ baseName = oldFileName.substring(0, dot);
+ }
+ }
+
+ String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)";
+ TextInputDialog dlg = new TextInputDialog(baseName);
+ dlg.setTitle("Umbenennen");
+ dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt);
+ dlg.setContentText("Neuer Name:");
+
+ final String fixedExt = ext;
+ dlg.showAndWait().ifPresent(raw -> {
+ String newBase = raw.trim();
+ if (newBase.isEmpty()) return;
+ // Append original extension for files
+ String newName = isFile ? newBase + fixedExt : newBase;
+ if (newName.equals(oldFileName)) return;
+ if (newName.contains("/") || newName.contains("\\")) {
+ setStatus("Ungültiger Name: keine Pfadtrennzeichen erlaubt.");
+ return;
+ }
+ Path newPath = oldPath.getParent().resolve(newName);
+ if (Files.exists(newPath)) {
+ setStatus("Fehler: " + newName + " existiert bereits.");
+ return;
+ }
+ try {
+ Files.move(oldPath, newPath);
+ itemPaths.put(item, newPath);
+ // Bei Ordnern alle Kind-Pfade anpassen
+ if (Files.isDirectory(newPath)) {
+ updateDescendantPaths(item, oldPath, newPath);
+ }
+ item.setValue(newName);
+ setStatus("Umbenannt: " + oldFileName + " → " + newName);
+ } catch (IOException ex) {
+ setStatus("Umbenennen fehlgeschlagen: " + ex.getMessage());
+ }
+ });
+ }
+
+ /** Aktualisiert itemPaths aller Kinder nach einer Ordner-Umbenennung. */
+ private void updateDescendantPaths(TreeItem parent, Path oldBase, Path newBase) {
+ for (TreeItem child : parent.getChildren()) {
+ Path p = itemPaths.get(child);
+ if (p != null) itemPaths.put(child, newBase.resolve(oldBase.relativize(p)));
+ updateDescendantPaths(child, oldBase, newBase);
+ }
+ }
+
+ /** Lädt Assets rekursiv: Ordner zuerst (alphabetisch), dann Dateien. */
+ private void loadAssetsRecursive(TreeItem parent, Path dir, String... exts) {
if (!Files.exists(dir)) return;
try (var stream = Files.list(dir)) {
- stream.filter(p -> {
- String name = p.getFileName().toString().toLowerCase();
- for (String ext : exts) if (name.endsWith(ext)) return true;
- return false;
- }).map(p -> new TreeItem<>(p.getFileName().toString()))
- .forEach(parent.getChildren()::add);
+ List paths = stream
+ .sorted(Comparator.comparing((Path p) -> Files.isDirectory(p) ? 0 : 1)
+ .thenComparing(p -> p.getFileName().toString().toLowerCase()))
+ .toList();
+ for (Path p : paths) {
+ if (Files.isDirectory(p)) {
+ TreeItem sub = new TreeItem<>(p.getFileName().toString());
+ itemPaths.put(sub, p);
+ parent.getChildren().add(sub);
+ loadAssetsRecursive(sub, p, exts);
+ } else {
+ String lo = p.getFileName().toString().toLowerCase();
+ for (String ext : exts) {
+ if (lo.endsWith(ext)) {
+ TreeItem file = new TreeItem<>(p.getFileName().toString());
+ itemPaths.put(file, p);
+ parent.getChildren().add(file);
+ break;
+ }
+ }
+ }
+ }
} catch (IOException ignored) {}
}
+ /** Baut den Teilbaum einer Kategorie komplett neu auf (nach Export/Import). */
+ private void refreshCategoryNode(TreeItem catNode, String... exts) {
+ clearItemPathsFor(catNode);
+ catNode.getChildren().clear();
+ Path dir = itemPaths.get(catNode);
+ if (dir != null) loadAssetsRecursive(catNode, dir, exts);
+ }
+
+ private void clearItemPathsFor(TreeItem item) {
+ for (TreeItem child : item.getChildren()) clearItemPathsFor(child);
+ if (item != modelsNode && item != texturesNode && item != audioNode)
+ itemPaths.remove(item);
+ }
+
private void handleImport(javafx.stage.Window owner) {
FileChooser fc = new FileChooser();
fc.setTitle("Assets importieren");
@@ -186,24 +1557,36 @@ public class EditorApp extends Application {
if (files == null) return;
for (File file : files) {
- String name = file.getName().toLowerCase();
- String subDir;
- TreeItem parent;
- if (name.matches(".*\\.(j3o|obj|fbx|gltf|glb)")) {
- subDir = "models"; parent = modelsNode;
- } else if (name.matches(".*\\.(ogg|wav|mp3)")) {
- subDir = "audio"; parent = audioNode;
- } else {
- subDir = "textures"; parent = texturesNode;
- }
+ String name = file.getName().toLowerCase();
+ boolean isNativeModel = name.matches(".*\\.(obj|fbx|gltf|glb)");
+ boolean isJ3o = name.endsWith(".j3o");
+ boolean isAudio = name.matches(".*\\.(ogg|wav|mp3)");
+ boolean isModel = isNativeModel || isJ3o;
+
+ String subDir = isModel ? "models" : isAudio ? "audio" : "textures";
+ TreeItem parent = isModel ? modelsNode : isAudio ? audioNode : texturesNode;
+
try {
- Path dest = ASSET_ROOT.resolve(subDir);
- Files.createDirectories(dest);
- Path target = dest.resolve(file.getName());
- Files.copy(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING);
- parent.getChildren().add(new TreeItem<>(file.getName()));
- parent.setExpanded(true);
- setStatus("Importiert: " + file.getName());
+ Path destDir = ASSET_ROOT.resolve(subDir);
+ Files.createDirectories(destDir);
+ Path destFile = destDir.resolve(file.getName());
+ Files.copy(file.toPath(), destFile, StandardCopyOption.REPLACE_EXISTING);
+
+ if (isNativeModel) {
+ // Asynchron via JME3 zu .j3o konvertieren
+ String assetPath = subDir + "/" + file.getName();
+ String baseName = file.getName().replaceFirst("\\.[^.]+$", "");
+ Path destJ3o = destDir.resolve(baseName + ".j3o");
+ input.modelConvertQueue.offer(
+ new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile));
+ setStatus("Konvertiere " + file.getName() + " → .j3o …");
+ } else {
+ TreeItem newItem = new TreeItem<>(file.getName());
+ itemPaths.put(newItem, destFile);
+ parent.getChildren().add(newItem);
+ parent.setExpanded(true);
+ setStatus("Importiert: " + file.getName());
+ }
} catch (IOException ex) {
setStatus("Fehler beim Import: " + ex.getMessage());
}
@@ -220,7 +1603,6 @@ public class EditorApp extends Application {
StackPane pane = new StackPane(viewport);
pane.setStyle("-fx-background-color: #1a1a2e;");
- // ImageView auf Pane-Größe binden → JME3-Pixelskalierung aktualisieren
pane.widthProperty().addListener((o, oldW, newW) -> {
viewport.setFitWidth(newW.doubleValue());
input.viewportScaleX = VP_WIDTH / newW.doubleValue();
@@ -230,43 +1612,81 @@ public class EditorApp extends Application {
input.viewportScaleY = VP_HEIGHT / newH.doubleValue();
});
- // ── Maus-Events ──────────────────────────────────────────────────────
viewport.setOnMousePressed(e -> {
viewport.requestFocus();
if (e.getButton() == MouseButton.MIDDLE) {
- prevDragX = e.getX();
- prevDragY = e.getY();
+ prevDragX = e.getX(); prevDragY = e.getY();
}
- if (e.getButton() == MouseButton.PRIMARY) {
- submitEdit(e.getX(), e.getY(), +1);
+ boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
+
+ if (isObjectMode()) {
+ if (e.getButton() == MouseButton.MIDDLE || bothDown) {
+ prevDragX = e.getX(); prevDragY = e.getY();
+ } else if (e.getButton() == MouseButton.PRIMARY) {
+ input.objectClickQueue.offer(
+ new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false));
+ objDragPrevX = e.getX(); objDragPrevY = e.getY();
+ objDragging = true;
+ }
+ return;
}
- if (e.getButton() == MouseButton.SECONDARY) {
- submitEdit(e.getX(), e.getY(), -1);
+
+ if (bothDown) {
+ stopEditTimer();
+ prevDragX = e.getX(); prevDragY = e.getY();
+ } else if (e.getButton() == MouseButton.PRIMARY) {
+ editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
+ submitEdit(editPressX, editPressY, editPressAction);
+ startEditTimer();
+ } else if (e.getButton() == MouseButton.SECONDARY) {
+ editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
+ submitEdit(editPressX, editPressY, editPressAction);
+ startEditTimer();
}
});
viewport.setOnMouseDragged(e -> {
- if (e.isMiddleButtonDown()) {
+ boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
+
+ if (isObjectMode()) {
+ if (e.isMiddleButtonDown() || bothDown) {
+ double dx = e.getX() - prevDragX;
+ double dy = e.getY() - prevDragY;
+ input.addMouseDelta((int) dx, (int) dy);
+ prevDragX = e.getX(); prevDragY = e.getY();
+ } else if (objDragging && e.isPrimaryButtonDown()) {
+ float dx = (float)(e.getX() - objDragPrevX);
+ float dy = (float)(e.getY() - objDragPrevY);
+ input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy));
+ objDragPrevX = e.getX(); objDragPrevY = e.getY();
+ }
+ return;
+ }
+
+ if (e.isMiddleButtonDown() || bothDown) {
+ stopEditTimer();
double dx = e.getX() - prevDragX;
double dy = e.getY() - prevDragY;
input.addMouseDelta((int) dx, (int) dy);
- prevDragX = e.getX();
- prevDragY = e.getY();
- }
- if (e.isPrimaryButtonDown()) {
- submitEdit(e.getX(), e.getY(), +1);
- }
- if (e.isSecondaryButtonDown()) {
- submitEdit(e.getX(), e.getY(), -1);
+ prevDragX = e.getX(); prevDragY = e.getY();
}
});
+ viewport.setOnMouseReleased(e -> {
+ objDragging = false;
+ if (!isObjectMode()) stopEditTimer();
+ });
+
+ pane.setOnMouseMoved(e -> {
+ input.mouseScreenX = (float) e.getX();
+ input.mouseScreenY = (float) e.getY();
+ });
+ pane.setOnMouseExited(e -> input.mouseScreenX = -1f);
+
viewport.setOnScroll(e -> {
- // Scrollen = Kamera vorwärts/rückwärts
double delta = e.getDeltaY();
input.forward = delta > 0;
input.backward = delta < 0;
- // Nach kurzem Delay zurücksetzen (kein physischer Key-Release)
javafx.animation.PauseTransition pause =
new javafx.animation.PauseTransition(javafx.util.Duration.millis(150));
pause.setOnFinished(ev -> { input.forward = false; input.backward = false; });
@@ -276,14 +1696,43 @@ public class EditorApp extends Application {
return pane;
}
+ private static void applyModeButtonStyle(ToggleButton btn, boolean active) {
+ if (active) {
+ btn.setStyle("-fx-border-color: #2277cc; -fx-border-width: 3; -fx-border-radius: 5;" +
+ "-fx-background-color: #d4e8ff; -fx-padding: 2;");
+ } else {
+ btn.setStyle("-fx-border-color: #bbbbbb; -fx-border-width: 1; -fx-border-radius: 5;" +
+ "-fx-background-color: #f4f4f4; -fx-padding: 4;");
+ }
+ }
+
+ private void startEditTimer() {
+ stopEditTimer();
+ editTimer = new javafx.animation.Timeline(
+ new javafx.animation.KeyFrame(javafx.util.Duration.millis(50),
+ ev -> submitEdit(editPressX, editPressY, editPressAction))
+ );
+ editTimer.setCycleCount(javafx.animation.Timeline.INDEFINITE);
+ editTimer.play();
+ }
+
+ private void stopEditTimer() {
+ if (editTimer != null) { editTimer.stop(); editTimer = null; }
+ }
+
private void submitEdit(double x, double y, int action) {
- input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
+ switch (input.activeLayer) {
+ case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
+ case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action));
+ case 4 -> input.textureEditQueue.offer(new SharedInput.TextureEdit((float) x, (float) y, action));
+ default -> input.upperLayerEditQueue.offer(new SharedInput.UpperLayerEdit((float) x, (float) y, action));
+ }
}
// ── Statusleiste ─────────────────────────────────────────────────────────
private HBox buildStatusBar() {
- statusLabel = new Label("Bereit | Werkzeug: Höhe | WASD/QE: Bewegen | Mitte-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.setStyle("-fx-font-size: 11; -fx-text-fill: #333;");
HBox bar = new HBox(statusLabel);
diff --git a/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java b/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java
index 81c05a1..35f5b6e 100644
--- a/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java
+++ b/blight-editor/src/main/java/de/blight/editor/EditorLauncher.java
@@ -1,11 +1,11 @@
-package de.blight.editor;
-
-/**
- * Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
- * aufgelöst wird, bevor Application.launch() aufgerufen wird.
- */
-public class EditorLauncher {
- public static void main(String[] args) {
- EditorApp.main(args);
- }
-}
+package de.blight.editor;
+
+/**
+ * Separater Launcher-Einstiegspunkt, damit der JavaFX-Klassenpfad korrekt
+ * aufgelöst wird, bevor Application.launch() aufgerufen wird.
+ */
+public class EditorLauncher {
+ public static void main(String[] args) {
+ EditorApp.main(args);
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/FrameTransfer.class b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.class
new file mode 100644
index 0000000..4a54da2
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java
index 6b133d4..6ab0312 100644
--- a/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java
+++ b/blight-editor/src/main/java/de/blight/editor/FrameTransfer.java
@@ -22,7 +22,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class FrameTransfer implements SceneProcessor {
- private final WritableImage image;
private final PixelWriter pw;
private final int width;
private final int height;
@@ -30,12 +29,11 @@ public class FrameTransfer implements SceneProcessor {
private Renderer renderer;
private ByteBuffer cpuBuf;
private byte[] snapshot;
- private int[] argbRow; // Zeile für JavaFX-PixelWriter
+ private int[] argbBuf; // gesamtes Bild für einmaligen bulk-Write
private final AtomicBoolean jfxBusy = new AtomicBoolean(false);
public FrameTransfer(WritableImage image) {
- this.image = image;
this.pw = image.getPixelWriter();
this.width = (int) image.getWidth();
this.height = (int) image.getHeight();
@@ -46,34 +44,39 @@ public class FrameTransfer implements SceneProcessor {
this.renderer = rm.getRenderer();
this.cpuBuf = ByteBuffer.allocateDirect(width * height * 4);
this.snapshot = new byte[width * height * 4];
- this.argbRow = new int[width];
+ this.argbBuf = new int[width * height];
}
@Override
public void postFrame(FrameBuffer out) {
if (!jfxBusy.compareAndSet(false, true)) return;
- cpuBuf.clear();
- renderer.readFrameBuffer(out, cpuBuf);
- cpuBuf.rewind();
- cpuBuf.get(snapshot);
+ try {
+ cpuBuf.clear();
+ renderer.readFrameBuffer(out, cpuBuf);
+ cpuBuf.rewind();
+ cpuBuf.get(snapshot);
+ } catch (Exception e) {
+ jfxBusy.set(false);
+ return;
+ }
final byte[] pixels = snapshot.clone();
Platform.runLater(() -> {
try {
- // GL: Y=0 unten → JavaFX: Y=0 oben + RGBA → 0xFFRRGGBB (int ARGB)
PixelFormat fmt = PixelFormat.getIntArgbInstance();
for (int y = 0; y < height; y++) {
int srcBase = (height - 1 - y) * width * 4;
+ int dstBase = y * width;
for (int x = 0; x < width; x++) {
int r = pixels[srcBase + x * 4 ] & 0xFF;
int g = pixels[srcBase + x * 4 + 1] & 0xFF;
int b = pixels[srcBase + x * 4 + 2] & 0xFF;
- argbRow[x] = 0xFF000000 | (r << 16) | (g << 8) | b;
+ argbBuf[dstBase + x] = 0xFF000000 | (r << 16) | (g << 8) | b;
}
- pw.setPixels(0, y, width, 1, fmt, argbRow, 0, width);
}
+ pw.setPixels(0, 0, width, height, fmt, argbBuf, 0, width);
} finally {
jfxBusy.set(false);
}
diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java
index 0749f91..38f1790 100644
--- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java
+++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java
@@ -1,53 +1,83 @@
-package de.blight.editor;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.system.AppSettings;
-import com.jme3.system.JmeContext;
-import de.blight.editor.state.TerrainEditorState;
-import javafx.scene.image.WritableImage;
-
-public class JmeEditorApp extends SimpleApplication {
-
- private final SharedInput input;
- private final WritableImage jfxImage;
-
- public JmeEditorApp(SharedInput input, WritableImage jfxImage) {
- this.input = input;
- this.jfxImage = jfxImage;
- }
-
- /** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
- public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
- int vpWidth, int vpHeight) {
- JmeEditorApp app = new JmeEditorApp(input, jfxImage);
-
- AppSettings settings = new AppSettings(true);
- settings.setTitle("Blight Editor – JME3");
- settings.setResolution(vpWidth, vpHeight);
- settings.setRenderer(AppSettings.LWJGL_OPENGL32);
- settings.setAudioRenderer(null);
- settings.setSamples(4);
-
- app.setSettings(settings);
- app.setShowSettings(false);
- app.setPauseOnLostFocus(false);
-
- Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
- t.setDaemon(true);
- t.start();
- return app;
- }
-
- @Override
- public void simpleInitApp() {
- flyCam.setEnabled(false);
-
- // Frame-Export in das JavaFX-WritableImage
- viewPort.addProcessor(new FrameTransfer(jfxImage));
-
- stateManager.attach(new TerrainEditorState(input));
- }
-
- @Override
- public void simpleUpdate(float tpf) {}
-}
+package de.blight.editor;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.plugins.FileLocator;
+import com.jme3.system.AppSettings;
+import com.jme3.system.JmeContext;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
+import de.blight.editor.state.EzTreeState;
+import de.blight.editor.state.PalmGeneratorState;
+import de.blight.editor.state.SceneObjectState;
+import de.blight.editor.state.TerrainEditorState;
+import de.blight.editor.state.TreeGeneratorState;
+import javafx.scene.image.WritableImage;
+
+public class JmeEditorApp extends SimpleApplication {
+
+ private final SharedInput input;
+ private final WritableImage jfxImage;
+ private final int vpWidth;
+ private final int vpHeight;
+
+ public JmeEditorApp(SharedInput input, WritableImage jfxImage, int vpWidth, int vpHeight) {
+ this.input = input;
+ this.jfxImage = jfxImage;
+ this.vpWidth = vpWidth;
+ this.vpHeight = vpHeight;
+ }
+
+ /** Startet JME3 in einem Daemon-Thread (blockierend bis App endet). */
+ public static JmeEditorApp launch(SharedInput input, WritableImage jfxImage,
+ int vpWidth, int vpHeight) {
+ JmeEditorApp app = new JmeEditorApp(input, jfxImage, vpWidth, vpHeight);
+
+ AppSettings settings = new AppSettings(true);
+ settings.setTitle("Blight Editor – JME3");
+ settings.setResolution(vpWidth, vpHeight);
+ settings.setRenderer(AppSettings.LWJGL_OPENGL32);
+ settings.setAudioRenderer(null);
+ settings.setSamples(1);
+
+ app.setSettings(settings);
+ app.setShowSettings(false);
+ app.setPauseOnLostFocus(false);
+
+ Thread t = new Thread(() -> app.start(JmeContext.Type.OffscreenSurface), "jme3-editor");
+ t.setDaemon(true);
+ t.start();
+ return app;
+ }
+
+ @Override
+ public void simpleInitApp() {
+ flyCam.setEnabled(false);
+ // editor-assets/ im AssetManager registrieren, damit Texturen und Modelle
+ // aus diesem Verzeichnis geladen werden können (relativ zum Arbeitsverzeichnis).
+ try {
+ assetManager.registerLocator(
+ java.nio.file.Paths.get("editor-assets").toAbsolutePath().toString(),
+ FileLocator.class);
+ } catch (Exception ignored) {}
+
+ // Texture2D-Attachment: readFrameBuffer() funktioniert nur mit Texture, nicht Renderbuffer
+ Texture2D colorTex = new Texture2D(vpWidth, vpHeight, Image.Format.RGBA8);
+ FrameBuffer fb = new FrameBuffer(vpWidth, vpHeight, 1);
+ fb.setDepthBuffer(Image.Format.Depth);
+ fb.setColorTexture(colorTex);
+ viewPort.setOutputFrameBuffer(fb);
+
+ // Frame-Export in das JavaFX-WritableImage
+ viewPort.addProcessor(new FrameTransfer(jfxImage));
+
+ stateManager.attach(new SceneObjectState(input));
+ stateManager.attach(new TerrainEditorState(input));
+ stateManager.attach(new TreeGeneratorState(input));
+ stateManager.attach(new EzTreeState(input));
+ stateManager.attach(new PalmGeneratorState(input));
+ }
+
+ @Override
+ public void simpleUpdate(float tpf) {}
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$GrassEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$GrassEdit.class
new file mode 100644
index 0000000..ec00d7f
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$GrassEdit.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$TerrainEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$TerrainEdit.class
new file mode 100644
index 0000000..a15aa23
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$TerrainEdit.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$TextureEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$TextureEdit.class
new file mode 100644
index 0000000..f9458c8
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$TextureEdit.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$TreeGenRequest.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$TreeGenRequest.class
new file mode 100644
index 0000000..a1d4c10
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$TreeGenRequest.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput$UpperLayerEdit.class b/blight-editor/src/main/java/de/blight/editor/SharedInput$UpperLayerEdit.class
new file mode 100644
index 0000000..df89e34
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput$UpperLayerEdit.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.class b/blight-editor/src/main/java/de/blight/editor/SharedInput.class
new file mode 100644
index 0000000..affed90
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/SharedInput.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java
index 6f6ebd7..f42dd1b 100644
--- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java
+++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java
@@ -1,11 +1,33 @@
package de.blight.editor;
+import de.blight.editor.tool.EditorTool;
+import de.blight.editor.tool.GrassTool;
+import de.blight.editor.tool.HeightTool;
+import de.blight.editor.tool.HoleTool;
+import de.blight.editor.tool.TextureTool;
+import de.blight.editor.tool.UpperHeightTool;
+import de.blight.editor.tree.PalmOptions;
+import de.blight.editor.tree.TreeParams;
+import javafx.scene.image.WritableImage;
+
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/** Thread-safe Brücke: JavaFX-Events → JME3-Update-Schleife. */
public class SharedInput {
+ // ── Aktive Tools ─────────────────────────────────────────────────────────
+ public final HeightTool heightTool = new HeightTool();
+ public final UpperHeightTool upperHeightTool = new UpperHeightTool();
+ public final HoleTool holeTool = new HoleTool();
+ public final GrassTool grassTool = new GrassTool();
+ 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 ──
+ public volatile int activeLayer = 0;
+ public volatile boolean upperLayerVisible = true;
+
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
public volatile boolean forward, backward, left, right, up, down;
@@ -18,7 +40,6 @@ public class SharedInput {
mouseDyAccum.addAndGet(dy);
}
- /** Gibt akkumulierten Maus-Delta zurück und setzt ihn zurück. */
public int[] consumeMouseDelta() {
return new int[]{ mouseDxAccum.getAndSet(0), mouseDyAccum.getAndSet(0) };
}
@@ -27,7 +48,119 @@ public class SharedInput {
public record TerrainEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue editQueue = new ConcurrentLinkedQueue<>();
+ // ── Upper-Layer-Edits ─────────────────────────────────────────────────────
+ public record UpperLayerEdit(float screenX, float screenY, int action) {}
+ public final ConcurrentLinkedQueue upperLayerEditQueue = new ConcurrentLinkedQueue<>();
+
+ // ── Gras-Edits ────────────────────────────────────────────────────────────
+ /** action +1 = Dichte erhöhen (Linksklick), -1 = Dichte verringern (Rechtsklick). */
+ public record GrassEdit(float screenX, float screenY, int action) {}
+ public final ConcurrentLinkedQueue grassEditQueue = new ConcurrentLinkedQueue<>();
+
+ // ── Textur-Edits ─────────────────────────────────────────────────────────
+ /** action +1 = selektierte Textur malen, -1 = auf Gras zurücksetzen. */
+ public record TextureEdit(float screenX, float screenY, int action) {}
+ public final ConcurrentLinkedQueue textureEditQueue = new ConcurrentLinkedQueue<>();
+
// ── Viewport-Skalierung (JavaFX-Pixel → JME3-Pixel) ─────────────────────
public volatile double viewportScaleX = 1.0;
public volatile double viewportScaleY = 1.0;
+
+ // ── Mausposition im Viewport (JavaFX-Pixel, -1 = außerhalb) ─────────────
+ public volatile float mouseScreenX = -1f;
+ public volatile float mouseScreenY = -1f;
+
+ // ── Speichern ─────────────────────────────────────────────────────────────
+ public volatile boolean saveRequested = false;
+ public volatile String saveStatusMsg = null;
+
+ // ── Vorschau-Viewport (gemeinsam für Baum-Generator & EZ-Tree) ───────────
+ public volatile float treePreviewRotY = 0f; // Yaw-Winkel in Grad
+ public volatile float treePreviewRotX = 30f; // Elevation in Grad [5, 80]
+ public volatile float treePreviewZoom = 1.0f; // Zoom-Faktor [0.25, 4.0]
+ /** Gewünschte Framebuffer-Größe – von JavaFX gesetzt, von JME3 gelesen. */
+ public volatile int treePreviewW = 1024;
+ public volatile int treePreviewH = 1024;
+ public volatile String treeGenStatusMsg = null;
+ public volatile boolean refreshAssets = false;
+ /**
+ * Aktuelles Vorschau-Bild. JME3 ersetzt die Referenz bei Größenänderung;
+ * treePreviewResized signalisiert JavaFX, die ImageView zu aktualisieren.
+ */
+ public volatile WritableImage treePreviewImage = new WritableImage(1024, 1024);
+ public volatile boolean treePreviewResized = false;
+
+ // ── Baum-Generator ───────────────────────────────────────────────────────
+ public record TreeGenRequest(TreeParams params, boolean exportAfter, String exportName) {}
+ public final ConcurrentLinkedQueue treeGenQueue = new ConcurrentLinkedQueue<>();
+
+ // ── EZ-Tree-Generator ─────────────────────────────────────────────────────
+ public record EzTreeGenRequest(de.blight.eztree.TreeOptions options, boolean exportAfter, String exportName) {}
+ public final ConcurrentLinkedQueue ezTreeGenQueue = new ConcurrentLinkedQueue<>();
+
+ // ── Palmen-Generator ──────────────────────────────────────────────────────
+ public record PalmGenRequest(PalmOptions options, boolean exportAfter, String exportName) {}
+ public final ConcurrentLinkedQueue palmGenQueue = new ConcurrentLinkedQueue<>();
+
+ // ── Objekt-Werkzeug ──────────────────────────────────────────────────────
+ /** activeLayer==5 → Objekte platzieren */
+ public static final int LAYER_OBJECTS = 5;
+ /** activeLayer==6 → Objekte bearbeiten (Selektion + Gizmo) */
+ public static final int LAYER_OBJECTS_EDIT = 6;
+
+ /** Klick im Viewport: Objekt auswählen oder am Terrain-Treffpunkt platzieren. */
+ public record ObjectClick(float screenX, float screenY, boolean rightButton) {}
+ public final ConcurrentLinkedQueue objectClickQueue = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Rohe Maus-Delta beim Drag im Objekt-Modus.
+ * JME3 projiziert dx/dy je nach aktivem Gizmo-Pfeil auf die Weltachse.
+ */
+ public record ObjectDrag(float dx, float dy) {}
+ public final ConcurrentLinkedQueue objectDragQueue = new ConcurrentLinkedQueue<>();
+
+ /** Wird von JME3 gesetzt wenn ein neues Objekt oder eine neue Selektion vorliegt. */
+ public volatile String selectedObjectInfo = null; // "modelPath|solid|x|y|z|rotY|scale"
+ public volatile boolean objectSelectionChanged = false;
+ /** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */
+ public volatile boolean objectJustPlaced = false;
+
+ /** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu editor-assets/). */
+ public volatile String pendingModelPath = null;
+
+ /** JavaFX → JME3: Solid-Flag des selektierten Objekts ändern. */
+ public volatile Boolean pendingSolidChange = null;
+
+ // ── Mesh-Erstellung ───────────────────────────────────────────────────────
+ /**
+ * Form: "Box" | "Kugel" | "Zylinder" | "Ebene"
+ * sizeX: Breite (Box/Ebene) oder Radius (Kugel/Zylinder)
+ * sizeY: Höhe (Box/Zylinder)
+ * sizeZ: Tiefe (Box/Ebene)
+ * matType: "Unshaded" | "Phong"
+ * texturePath: relativ zu editor-assets/ oder null
+ */
+ public record MeshCreateRequest(
+ String form,
+ float sizeX, float sizeY, float sizeZ,
+ String matType,
+ float r, float g, float b, float a,
+ String texturePath,
+ boolean wireframe,
+ String name
+ ) {}
+ public final ConcurrentLinkedQueue meshCreateQueue =
+ new ConcurrentLinkedQueue<>();
+
+ // ── Modell-Konvertierung ──────────────────────────────────────────────────
+ /**
+ * Konvertiert ein natives Modell (OBJ/GLTF/…) zu .j3o.
+ * assetPath : Pfad relativ zu editor-assets/ (z. B. "models/tree.obj")
+ * destJ3o : absoluter Ziel-Pfad der .j3o-Datei
+ * srcToDelete: absoluter Pfad der Original-Datei (wird nach Konvertierung gelöscht)
+ */
+ public record ModelConvertRequest(String assetPath, java.nio.file.Path destJ3o,
+ java.nio.file.Path srcToDelete) {}
+ public final ConcurrentLinkedQueue modelConvertQueue =
+ new ConcurrentLinkedQueue<>();
}
diff --git a/blight-editor/src/main/java/de/blight/editor/object/GrassObject.java b/blight-editor/src/main/java/de/blight/editor/object/GrassObject.java
new file mode 100644
index 0000000..56d3c9c
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/object/GrassObject.java
@@ -0,0 +1,15 @@
+package de.blight.editor.object;
+
+public final class GrassObject extends PlacedObject {
+
+ /** Klingenhöhe in Welteinheiten. */
+ public final float height;
+
+ public GrassObject(float worldX, float worldZ, float groundY, float height) {
+ super(worldX, worldZ, groundY);
+ this.height = height;
+ }
+
+ @Override
+ public String getType() { return "grass"; }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/object/PlacedObject.java b/blight-editor/src/main/java/de/blight/editor/object/PlacedObject.java
new file mode 100644
index 0000000..4e30fb8
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/object/PlacedObject.java
@@ -0,0 +1,29 @@
+package de.blight.editor.object;
+
+/**
+ * Abstrakte Basisklasse für alle Objekte, die auf der Karte platziert werden.
+ * Die horizontale Position (worldX, worldZ) ist unveränderlich.
+ * groundY folgt dem Terrain wenn dieses angehoben/abgesenkt wird.
+ */
+public abstract class PlacedObject {
+
+ protected final float worldX;
+ protected final float worldZ;
+ protected float groundY; // Geländeoberfläche am Standort – wird bei Terrain-Edit angepasst
+
+ protected PlacedObject(float worldX, float worldZ, float groundY) {
+ this.worldX = worldX;
+ this.worldZ = worldZ;
+ this.groundY = groundY;
+ }
+
+ /** Eindeutiger Typ-Name (z. B. "grass"). */
+ public abstract String getType();
+
+ public float getWorldX() { return worldX; }
+ public float getWorldZ() { return worldZ; }
+ public float getGroundY() { return groundY; }
+
+ /** Wird aufgerufen wenn sich das Terrain an dieser Stelle hebt oder senkt. */
+ public void adjustGroundY(float delta) { groundY += delta; }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java b/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java
new file mode 100644
index 0000000..f7f7a91
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/object/SceneObject.java
@@ -0,0 +1,43 @@
+package de.blight.editor.object;
+
+/**
+ * Ein platziertes 3D-Objekt auf der Karte.
+ * X/Z sind jetzt veränderlich (per Gizmo verschiebbar).
+ */
+public class SceneObject extends PlacedObject {
+
+ private float worldXMut;
+ private float worldZMut;
+ private float rotY; // Y-Achsen-Rotation in Radiant
+ private float scale;
+ public boolean solid; // Charakter-Kollision
+ public String modelPath; // relativ zu editor-assets/
+
+ public SceneObject(String modelPath, float worldX, float worldZ, float groundY,
+ boolean solid) {
+ super(worldX, worldZ, groundY);
+ this.worldXMut = worldX;
+ this.worldZMut = worldZ;
+ this.rotY = 0f;
+ this.scale = 1f;
+ this.solid = solid;
+ this.modelPath = modelPath;
+ }
+
+ @Override public String getType() { return "sceneObject"; }
+
+ @Override public float getWorldX() { return worldXMut; }
+ @Override public float getWorldZ() { return worldZMut; }
+
+ public float getRotY() { return rotY; }
+ public float getScale() { return scale; }
+
+ public void translate(float dx, float dy, float dz) {
+ worldXMut += dx;
+ groundY += dy;
+ worldZMut += dz;
+ }
+
+ public void rotateY(float deltaRad) { rotY += deltaRad; }
+ public void setScale(float s) { scale = s; }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java
new file mode 100644
index 0000000..be3f10a
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java
@@ -0,0 +1,349 @@
+package de.blight.editor.state;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.post.SceneProcessor;
+import com.jme3.profile.AppProfiler;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.BufferUtils;
+import de.blight.editor.SharedInput;
+import de.blight.eztree.Tree;
+import de.blight.eztree.TreeOptions;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * JME3-AppState für den EZ-Tree-Generator.
+ *
+ * Teilt den Vorschau-Viewport mit {@link TreeGeneratorState} (kein eigenes Framebuffer).
+ * Verarbeitet {@link SharedInput.EzTreeGenRequest}-Einträge aus der Queue,
+ * baut einen {@link Tree}-Node, weist Materialien zu und zeigt ihn in der Vorschau.
+ * Optional: .j3o-Export mit Impostor-PNG.
+ */
+public class EzTreeState extends BaseAppState {
+
+ private static final int IMPOSTOR_SIZE = 512;
+ private static final Path ASSET_ROOT = Paths.get("editor-assets");
+
+ private final SharedInput input;
+ private SimpleApplication app;
+ private AssetManager assets;
+ private TreeGeneratorState previewHost;
+
+ // ── Laufende Capture-Operation ────────────────────────────────────────────
+ private SharedInput.EzTreeGenRequest pendingRequest = null;
+ private Node pendingTreeNode = null;
+ private ViewPort captureVP = null;
+ private FrameBuffer captureFB = null;
+ private volatile boolean captureReady = false;
+
+ public EzTreeState(SharedInput input) { this.input = input; }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.assets = app.getAssetManager();
+ // previewHost via lazy-init in update() – TreeGeneratorState evtl. noch nicht attached
+ }
+
+ @Override protected void cleanup(Application app) { cleanupCapture(); }
+ @Override protected void onEnable() {}
+ @Override protected void onDisable() {}
+
+ // ── Update-Schleife ───────────────────────────────────────────────────────
+
+ @Override
+ public void update(float tpf) {
+ // Lazy-init: TreeGeneratorState muss initialisiert sein, bevor wir darauf zugreifen
+ if (previewHost == null) {
+ previewHost = getStateManager().getState(TreeGeneratorState.class);
+ if (previewHost == null) return;
+ }
+
+ if (pendingRequest != null && captureReady) {
+ finishCapture();
+ } else if (pendingRequest == null) {
+ SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
+ if (req != null) startGeneration(req);
+ }
+ }
+
+ // ── Phase 1: Generierung ──────────────────────────────────────────────────
+
+ private void startGeneration(SharedInput.EzTreeGenRequest req) {
+ cleanupCapture();
+
+ Tree tree = new Tree(req.options());
+ tree.generate();
+ applyMaterials(tree, req.options());
+ tree.updateGeometricState();
+
+ BoundingBox bb = boundsOf(tree);
+ float camDist = bb != null
+ ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
+ : 20f;
+ Vector3f target = bb != null
+ ? new Vector3f(0f, bb.getCenter().y, 0f)
+ : new Vector3f(0f, 5f, 0f);
+
+ // Szenenänderung über enqueue() – läuft am Anfang des nächsten Frames,
+ // bevor TreeGeneratorState.update() updateGeometricState() aufruft.
+ final float dist = camDist;
+ final Vector3f tgt = target;
+ app.enqueue(() -> {
+ previewHost.setPreviewContent(tree, dist, tgt);
+ if (req.exportAfter()) {
+ setupCapture(tree, boundsOf(tree), req);
+ }
+ });
+
+ if (!req.exportAfter()) {
+ input.treeGenStatusMsg = "EZ-Tree Vorschau: '" + req.exportName() + "'";
+ } else {
+ input.treeGenStatusMsg = "EZ-Tree: generiere…";
+ }
+ }
+
+ // ── Phase 2: Impostor-Capture ─────────────────────────────────────────────
+
+ private void setupCapture(Tree tree, BoundingBox bb, SharedInput.EzTreeGenRequest req) {
+ BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
+
+ Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
+ captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
+ captureFB.addColorTexture(capTex);
+ captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
+
+ captureVP = buildCaptureViewPort(tree, safeBb, captureFB);
+ captureReady = false;
+ pendingRequest = req;
+ pendingTreeNode = tree;
+ input.treeGenStatusMsg = "EZ-Tree: rendere Impostor…";
+ }
+
+ private void finishCapture() {
+ ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
+ app.getRenderer().readFrameBuffer(captureFB, pixels);
+ cleanupCapture();
+
+ saveImpostor(pixels, "ez_impostor_" + pendingRequest.exportName());
+ exportTree(pendingTreeNode, pendingRequest.exportName());
+
+ pendingRequest = null;
+ pendingTreeNode = null;
+ }
+
+ // ── Material-Aufbau ───────────────────────────────────────────────────────
+
+ private void applyMaterials(Tree tree, TreeOptions opts) {
+ for (Spatial child : tree.getChildren()) {
+ if (child instanceof Geometry g) {
+ switch (g.getName()) {
+ case "bark" -> {
+ g.setMaterial(buildBarkMat(opts));
+ g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ }
+ case "leaves" -> {
+ g.setMaterial(buildLeafMat(opts));
+ g.setQueueBucket(RenderQueue.Bucket.Transparent);
+ g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ }
+ }
+ } else if (child instanceof Node trellis) {
+ // Trellis-Node: Rinden-Material auf alle Geometrien
+ Material mat = buildBarkMat(opts);
+ for (Spatial s : trellis.getChildren()) {
+ if (s instanceof Geometry g) g.setMaterial(mat.clone());
+ }
+ }
+ }
+ }
+
+ private Material buildBarkMat(TreeOptions opts) {
+ try {
+ Material mat = new Material(assets, "MatDefs/Tree.j3md");
+ mat.setColor("Diffuse", new ColorRGBA(opts.bark.r, opts.bark.g, opts.bark.b, 1f));
+ mat.setFloat("WindStrength", 0.15f);
+ mat.setFloat("WindSpeed", 0.5f);
+ if (opts.bark.textureFile != null) {
+ try {
+ mat.setTexture("BarkMap", assets.loadTexture(opts.bark.textureFile));
+ mat.setBoolean("HasBarkMap", true);
+ } catch (Exception ignored) {}
+ }
+ return mat;
+ } catch (Exception e) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(opts.bark.r, opts.bark.g, opts.bark.b, 1f));
+ return mat;
+ }
+ }
+
+ private Material buildLeafMat(TreeOptions opts) {
+ try {
+ Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
+ mat.setColor("Diffuse", new ColorRGBA(opts.leaves.r, opts.leaves.g, opts.leaves.b, 1f));
+ mat.setFloat("WindStrength", 0.30f);
+ mat.setFloat("WindSpeed", 0.7f);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ if (opts.leaves.textureFile != null) {
+ try {
+ mat.setTexture("LeafMap", assets.loadTexture(opts.leaves.textureFile));
+ mat.setBoolean("HasLeafMap", true);
+ } catch (Exception ignored) {}
+ }
+ return mat;
+ } catch (Exception e) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(opts.leaves.r, opts.leaves.g, opts.leaves.b, 1f));
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ }
+ }
+
+ // ── Offscreen-Viewport für Impostor ───────────────────────────────────────
+
+ private ViewPort buildCaptureViewPort(Tree src, BoundingBox bb, FrameBuffer fb) {
+ Vector3f center = bb.getCenter().add(0f, 2f, 0f);
+ float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
+ float dist = extent * 3f;
+
+ Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
+ cam.setLocation(center.add(0f, 0f, dist));
+ cam.lookAt(center, Vector3f.UNIT_Y);
+ cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
+
+ ViewPort vp = app.getRenderManager()
+ .createPostView("ezCapture_" + System.nanoTime(), cam);
+ vp.setOutputFrameBuffer(fb);
+ vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
+ vp.setClearFlags(true, true, true);
+
+ Node scene = new Node("ezCapScene");
+ scene.addLight(new DirectionalLight(
+ new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
+ scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
+ scene.attachChild(cloneForCapture(src));
+ vp.attachScene(scene);
+ scene.updateGeometricState();
+
+ vp.addProcessor(new SceneProcessor() {
+ @Override public void initialize(RenderManager rm, ViewPort v) {}
+ @Override public void reshape(ViewPort v, int w, int h) {}
+ @Override public boolean isInitialized() { return true; }
+ @Override public void preFrame(float t) {}
+ @Override public void postQueue(RenderQueue rq) {}
+ @Override public void cleanup() {}
+ @Override public void setProfiler(AppProfiler p) {}
+ @Override
+ public void postFrame(FrameBuffer out) {
+ vp.removeProcessor(this);
+ captureReady = true;
+ }
+ });
+ return vp;
+ }
+
+ private static Node cloneForCapture(Tree src) {
+ Node copy = new Node("ezCap");
+ copy.setLocalTranslation(src.getLocalTranslation());
+ for (Spatial child : src.getChildren()) {
+ if (child instanceof Geometry g) {
+ Geometry gc = new Geometry(g.getName(), g.getMesh());
+ gc.setMaterial(g.getMaterial().clone());
+ copy.attachChild(gc);
+ } else if (child instanceof Node n) {
+ Node nc = new Node(n.getName());
+ for (Spatial ns : n.getChildren()) {
+ if (ns instanceof Geometry ng) {
+ Geometry ngc = new Geometry(ng.getName(), ng.getMesh());
+ ngc.setMaterial(ng.getMaterial().clone());
+ nc.attachChild(ngc);
+ }
+ }
+ copy.attachChild(nc);
+ }
+ }
+ return copy;
+ }
+
+ // ── Hilfsmethoden ─────────────────────────────────────────────────────────
+
+ private static BoundingBox boundsOf(Tree tree) {
+ if (tree.getWorldBound() instanceof BoundingBox bb) return bb;
+ return null;
+ }
+
+ private void cleanupCapture() {
+ if (captureVP != null) {
+ app.getRenderManager().removePostView(captureVP);
+ captureVP = null;
+ }
+ if (captureFB != null) {
+ try { captureFB.dispose(); } catch (Exception ignored) {}
+ captureFB = null;
+ }
+ captureReady = false;
+ }
+
+ private void saveImpostor(ByteBuffer pixels, String name) {
+ try {
+ pixels.rewind();
+ BufferedImage img = new BufferedImage(
+ IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
+ for (int y = 0; y < IMPOSTOR_SIZE; y++) {
+ for (int x = 0; x < IMPOSTOR_SIZE; x++) {
+ int r = pixels.get() & 0xFF, g = pixels.get() & 0xFF,
+ b = pixels.get() & 0xFF, a = pixels.get() & 0xFF;
+ img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
+ }
+ }
+ Path texDir = ASSET_ROOT.resolve("textures");
+ Files.createDirectories(texDir);
+ ImageIO.write(img, "PNG", texDir.resolve(name + ".png").toFile());
+ } catch (IOException e) {
+ System.err.println("[EzTreeState] Impostor-Fehler: " + e.getMessage());
+ }
+ }
+
+ private void exportTree(Node treeNode, String name) {
+ try {
+ Path modelDir = ASSET_ROOT.resolve("models");
+ Files.createDirectories(modelDir);
+ File out = modelDir.resolve("EzTree_" + name + ".j3o").toFile();
+ BinaryExporter.getInstance().save(treeNode, out);
+ input.treeGenStatusMsg = "EZ-Tree exportiert: " + out.getName();
+ input.refreshAssets = true;
+ } catch (IOException e) {
+ input.treeGenStatusMsg = "EZ-Tree Export-Fehler: " + e.getMessage();
+ }
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java
new file mode 100644
index 0000000..196bcb2
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java
@@ -0,0 +1,158 @@
+package de.blight.editor.state;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import de.blight.editor.SharedInput;
+import de.blight.editor.tree.PalmMeshBuilder;
+import de.blight.editor.tree.PalmOptions;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class PalmGeneratorState extends BaseAppState {
+
+ private static final Path ASSET_ROOT = Paths.get("editor-assets");
+
+ private final SharedInput input;
+ private SimpleApplication app;
+ private AssetManager assets;
+ private TreeGeneratorState previewHost;
+
+ public PalmGeneratorState(SharedInput input) { this.input = input; }
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.assets = app.getAssetManager();
+ }
+
+ @Override protected void cleanup(Application app) {}
+ @Override protected void onEnable() {}
+ @Override protected void onDisable() {}
+
+ @Override
+ public void update(float tpf) {
+ if (previewHost == null) {
+ previewHost = getStateManager().getState(TreeGeneratorState.class);
+ if (previewHost == null) return;
+ }
+
+ SharedInput.PalmGenRequest req = input.palmGenQueue.poll();
+ if (req == null) return;
+
+ Node palm = PalmMeshBuilder.build(req.options());
+ applyMaterials(palm, req.options());
+ palm.updateGeometricState();
+
+ BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null;
+ float dist = bb != null
+ ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f
+ : 20f;
+ Vector3f target = bb != null
+ ? new Vector3f(0f, bb.getCenter().y, 0f)
+ : new Vector3f(0f, 6f, 0f);
+
+ final float finalDist = dist;
+ final Vector3f finalTarget = target;
+ final Node finalPalm = palm;
+ final PalmOptions finalOpts = req.options();
+ final String finalName = req.exportName();
+ final boolean doExport = req.exportAfter();
+
+ app.enqueue(() -> {
+ previewHost.setPreviewContent(finalPalm, finalDist, finalTarget);
+ if (doExport) exportPalm(finalPalm, finalName);
+ });
+
+ input.treeGenStatusMsg = doExport
+ ? "Palme: exportiere…"
+ : "Palme: Vorschau '" + req.exportName() + "'";
+ }
+
+ private void applyMaterials(Node palm, PalmOptions opts) {
+ for (Spatial child : palm.getChildren()) {
+ if (!(child instanceof Geometry g)) continue;
+ switch (g.getName()) {
+ case "bark" -> {
+ g.setMaterial(buildBarkMat(opts));
+ g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ }
+ case "leaves" -> {
+ g.setMaterial(buildLeafMat(opts));
+ g.setQueueBucket(RenderQueue.Bucket.Transparent);
+ g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ }
+ }
+ }
+ }
+
+ private Material buildBarkMat(PalmOptions opts) {
+ try {
+ Material mat = new Material(assets, "MatDefs/Tree.j3md");
+ mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f));
+ mat.setFloat("WindStrength", 0.08f);
+ mat.setFloat("WindSpeed", 0.4f);
+ if (opts.barkTexture != null) {
+ try {
+ mat.setTexture("BarkMap", assets.loadTexture(opts.barkTexture));
+ mat.setBoolean("HasBarkMap", true);
+ } catch (Exception ignored) {}
+ }
+ return mat;
+ } catch (Exception e) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f));
+ return mat;
+ }
+ }
+
+ private Material buildLeafMat(PalmOptions opts) {
+ try {
+ Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
+ mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f));
+ mat.setFloat("WindStrength", 0.20f);
+ mat.setFloat("WindSpeed", 0.5f);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ if (opts.leafTexture != null) {
+ try {
+ mat.setTexture("LeafMap", assets.loadTexture(opts.leafTexture));
+ mat.setBoolean("HasLeafMap", true);
+ } catch (Exception ignored) {}
+ }
+ return mat;
+ } catch (Exception e) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f));
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ }
+ }
+
+ private void exportPalm(Node palmNode, String name) {
+ try {
+ Path modelDir = ASSET_ROOT.resolve("models");
+ Files.createDirectories(modelDir);
+ File out = modelDir.resolve("Palm_" + name + ".j3o").toFile();
+ BinaryExporter.getInstance().save(palmNode, out);
+ input.treeGenStatusMsg = "Palme exportiert: " + out.getName();
+ input.refreshAssets = true;
+ } catch (IOException e) {
+ input.treeGenStatusMsg = "Palme Export-Fehler: " + e.getMessage();
+ }
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState$GrassVisibilityControl.class b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState$GrassVisibilityControl.class
new file mode 100644
index 0000000..c04ad5d
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState$GrassVisibilityControl.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.class b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.class
new file mode 100644
index 0000000..9209db8
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java
new file mode 100644
index 0000000..2992830
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java
@@ -0,0 +1,343 @@
+package de.blight.editor.state;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.collision.CollisionResults;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.*;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.util.BufferUtils;
+import de.blight.common.MapData;
+import de.blight.editor.SharedInput;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.*;
+
+/**
+ * Rendert Gras auf dem Basis-Terrain.
+ *
+ * Datenmodell: Dichte-Map (513×513 Bytes, gleiche Auflösung wie Splatmap).
+ * Rendering: Pro 128×128-WE-Chunk ein gebatchtes Kreuz-Quad-Mesh.
+ * LOD: GrassVisibilityControl cullt Chunks jenseits FAR_DIST.
+ * Wind: MatDefs/Grass.j3md (Vertex-Shader mit Sinus-Wind).
+ */
+public class PlacedObjectState extends BaseAppState {
+
+ // ── Terrain-Konstanten ────────────────────────────────────────────────────
+ private static final int TERRAIN_HALF = 2048;
+ private static final float WORLD_SIZE = 4096f;
+
+ // ── Dichte-Map ────────────────────────────────────────────────────────────
+ private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; // 513
+ private static final float SPLAT_WE_PER_PX = WORLD_SIZE / (SPLAT_SIZE - 1); // 8.0
+
+ // ── Chunks ────────────────────────────────────────────────────────────────
+ private static final int CHUNK_SIZE = 128;
+ private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE; // 32
+ private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS;
+
+ // ── Gras-Generierung ──────────────────────────────────────────────────────
+ private static final int MAX_BLADES_PER_PIXEL = 3;
+ private static final float BLADE_WIDTH_FACTOR = 0.18f;
+
+ // ── LOD ───────────────────────────────────────────────────────────────────
+ private static final float GRASS_FAR_DIST = 400f;
+ private static final float GRASS_FAR_DIST_SQ = GRASS_FAR_DIST * GRASS_FAR_DIST;
+
+ // ── Rebuild-Budget ────────────────────────────────────────────────────────
+ private static final int MAX_REBUILDS_PER_FRAME = 3;
+
+ // ── Zustand ───────────────────────────────────────────────────────────────
+ private final SharedInput input;
+ private Camera cam;
+ private TerrainQuad terrain;
+
+ private Node grassNode;
+ private Material grassMat;
+
+ private byte[] densityMap;
+
+ private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT];
+ private final Geometry[] chunkGeos = new Geometry[CHUNK_COUNT];
+
+ // ── Konstruktor ───────────────────────────────────────────────────────────
+
+ public PlacedObjectState(SharedInput input, MapData loadedData) {
+ this.input = input;
+ this.densityMap = new byte[SPLAT_SIZE * SPLAT_SIZE];
+ if (loadedData != null && loadedData.grassDensity != null) {
+ System.arraycopy(loadedData.grassDensity, 0, densityMap, 0, densityMap.length);
+ Arrays.fill(dirtyChunks, true);
+ }
+ }
+
+ public void setTerrain(TerrainQuad terrain) {
+ this.terrain = terrain;
+ }
+
+ /** Gibt die aktuelle Dichte-Map zurück (für performSave). */
+ public byte[] getDensityMap() { return densityMap; }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
+
+ @Override
+ protected void initialize(Application app) {
+ this.cam = app.getCamera();
+ grassNode = new Node("grassNode");
+ ((SimpleApplication) app).getRootNode().attachChild(grassNode);
+ grassMat = buildGrassMaterial(app.getAssetManager());
+ }
+
+ @Override
+ protected void cleanup(Application app) {
+ ((SimpleApplication) app).getRootNode().detachChild(grassNode);
+ }
+
+ @Override protected void onEnable() { grassNode.setCullHint(Spatial.CullHint.Inherit); }
+ @Override protected void onDisable() { grassNode.setCullHint(Spatial.CullHint.Always); }
+
+ @Override
+ public void update(float tpf) {
+ processGrassEdits();
+ rebuildDirtyChunks();
+ }
+
+ // ── Material ──────────────────────────────────────────────────────────────
+
+ private Material buildGrassMaterial(AssetManager assets) {
+ try {
+ Material mat = new Material(assets, "MatDefs/Grass.j3md");
+ mat.setColor("Color", new ColorRGBA(0.25f, 0.70f, 0.15f, 1f));
+ mat.setFloat("WindSpeed", 0.5f);
+ mat.setFloat("WindStrength", 0.12f);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ } catch (Exception e) {
+ System.err.println("[PlacedObjectState] Grass.j3md nicht gefunden, Fallback: " + e.getMessage());
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f));
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ }
+ }
+
+ // ── Pinsel: Dichte-Map anpassen ───────────────────────────────────────────
+
+ private void processGrassEdits() {
+ SharedInput.GrassEdit edit;
+ while ((edit = input.grassEditQueue.poll()) != null) {
+ if (terrain == null) continue;
+ float jmeX = (float)(edit.screenX() * input.viewportScaleX);
+ float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
+ com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
+ CollisionResults hits = new CollisionResults();
+ terrain.collideWith(ray, hits);
+ if (hits.size() == 0) continue;
+ Vector3f contact = hits.getClosestCollision().getContactPoint();
+ float radius = (float) input.grassTool.brushRadius.getValue();
+ paintDensity(contact.x, contact.z, radius, edit.action());
+ }
+ }
+
+ private void paintDensity(float cx, float cz, float radius, int action) {
+ int centerPX = Math.round((cx + TERRAIN_HALF) / SPLAT_WE_PER_PX);
+ int centerPZ = Math.round((cz + TERRAIN_HALF) / SPLAT_WE_PER_PX);
+ int pixR = (int) Math.ceil(radius / SPLAT_WE_PER_PX);
+ float strength = (float) input.grassTool.density.getValue() / 10f; // 0.1–5.0
+
+ for (int dz = -pixR; dz <= pixR; dz++) {
+ int pz = centerPZ + dz;
+ if (pz < 0 || pz >= SPLAT_SIZE) continue;
+ for (int dx = -pixR; dx <= pixR; dx++) {
+ int px = centerPX + dx;
+ if (px < 0 || px >= SPLAT_SIZE) continue;
+ float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
+ if (distWE >= radius) continue;
+ float t = distWE / radius;
+ float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
+ int delta = (int)(strength * falloff * 40f);
+ int idx = pz * SPLAT_SIZE + px;
+ int cur = densityMap[idx] & 0xFF;
+ int nxt = (action > 0)
+ ? Math.min(255, cur + delta)
+ : Math.max(0, cur - delta);
+ if (nxt != cur) {
+ densityMap[idx] = (byte) nxt;
+ markChunkDirtyAtPixel(px, pz);
+ }
+ }
+ }
+ }
+
+ // ── Höhenanpassung bei Terrain-Edit ───────────────────────────────────────
+
+ /**
+ * Markiert alle Chunks dirty, deren Fläche eine der übergebenen Terrain-Positionen
+ * enthält. Die Blatt-Y-Koordinaten werden beim nächsten Rebuild neu von
+ * terrain.getHeight() abgelesen.
+ */
+ public void adjustObjectHeights(List locs, List deltas) {
+ for (Vector2f loc : locs) {
+ int cx = (int)((loc.x + TERRAIN_HALF) / CHUNK_SIZE);
+ int cz = (int)((loc.y + TERRAIN_HALF) / CHUNK_SIZE);
+ if (cx >= 0 && cx < CHUNKS_PER_AXIS && cz >= 0 && cz < CHUNKS_PER_AXIS) {
+ dirtyChunks[cx + cz * CHUNKS_PER_AXIS] = true;
+ }
+ }
+ }
+
+ // ── Chunk-Rebuild ─────────────────────────────────────────────────────────
+
+ private void rebuildDirtyChunks() {
+ int rebuilt = 0;
+ for (int i = 0; i < CHUNK_COUNT && rebuilt < MAX_REBUILDS_PER_FRAME; i++) {
+ if (!dirtyChunks[i]) continue;
+ rebuildChunk(i);
+ dirtyChunks[i] = false;
+ rebuilt++;
+ }
+ }
+
+ private void rebuildChunk(int idx) {
+ if (terrain == null) return;
+
+ int cx = idx % CHUNKS_PER_AXIS;
+ int cz = idx / CHUNKS_PER_AXIS;
+ float wXMin = -TERRAIN_HALF + cx * CHUNK_SIZE;
+ float wZMin = -TERRAIN_HALF + cz * CHUNK_SIZE;
+
+ // Dichte-Pixel-Bereich dieses Chunks
+ int pxMin = Math.max(0, (int)((wXMin + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+ int pzMin = Math.max(0, (int)((wZMin + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+ int pxMax = Math.min(SPLAT_SIZE - 1, (int)((wXMin + CHUNK_SIZE + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+ int pzMax = Math.min(SPLAT_SIZE - 1, (int)((wZMin + CHUNK_SIZE + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+
+ float baseH = (float) input.grassTool.grassHeight.getValue();
+
+ // Blatt-Positionen generieren
+ List blades = new ArrayList<>(); // [x, y, z, height]
+ for (int pz = pzMin; pz <= pzMax; pz++) {
+ for (int px = pxMin; px <= pxMax; px++) {
+ int d = densityMap[pz * SPLAT_SIZE + px] & 0xFF;
+ if (d == 0) continue;
+ int count = Math.max(1, (int)(d / 255f * MAX_BLADES_PER_PIXEL));
+ Random rng = new Random((long) px * 100003L + pz);
+ float pixWorldX = px * SPLAT_WE_PER_PX - TERRAIN_HALF;
+ float pixWorldZ = pz * SPLAT_WE_PER_PX - TERRAIN_HALF;
+ for (int b = 0; b < count; b++) {
+ float bx = pixWorldX + (rng.nextFloat() - 0.5f) * SPLAT_WE_PER_PX;
+ float bz = pixWorldZ + (rng.nextFloat() - 0.5f) * SPLAT_WE_PER_PX;
+ float th = terrain.getHeight(new Vector2f(bx, bz));
+ if (Float.isNaN(th)) continue;
+ float h = baseH * (0.7f + rng.nextFloat() * 0.6f);
+ blades.add(new float[]{bx, th, bz, h});
+ }
+ }
+ }
+
+ // Alte Geometrie entfernen
+ if (chunkGeos[idx] != null) {
+ grassNode.detachChild(chunkGeos[idx]);
+ chunkGeos[idx] = null;
+ }
+ if (blades.isEmpty()) return;
+
+ Mesh mesh = buildGrassMesh(blades);
+ float chunkCX = wXMin + CHUNK_SIZE * 0.5f;
+ float chunkCZ = wZMin + CHUNK_SIZE * 0.5f;
+ Geometry geo = new Geometry("grassChunk_" + idx, mesh);
+ geo.setMaterial(grassMat);
+ geo.addControl(new GrassVisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ)));
+ grassNode.attachChild(geo);
+ chunkGeos[idx] = geo;
+ }
+
+ // ── Mesh: Kreuz-Quad pro Halm mit UV-Koordinaten ──────────────────────────
+
+ private static Mesh buildGrassMesh(List blades) {
+ int n = blades.size();
+ FloatBuffer pos = BufferUtils.createFloatBuffer(n * 8 * 3);
+ FloatBuffer uv = BufferUtils.createFloatBuffer(n * 8 * 2);
+ IntBuffer idx = BufferUtils.createIntBuffer(n * 12);
+
+ int vi = 0;
+ for (float[] blade : blades) {
+ float x = blade[0], y = blade[1], z = blade[2], h = blade[3];
+ float w = Math.max(0.05f, h * BLADE_WIDTH_FACTOR);
+
+ // Quad A – Breite entlang X-Achse
+ pos.put(x-w).put(y ).put(z); uv.put(0).put(0);
+ pos.put(x+w).put(y ).put(z); uv.put(1).put(0);
+ pos.put(x+w).put(y+h).put(z); uv.put(1).put(1);
+ pos.put(x-w).put(y+h).put(z); uv.put(0).put(1);
+
+ // Quad B – Breite entlang Z-Achse
+ pos.put(x).put(y ).put(z-w); uv.put(0).put(0);
+ pos.put(x).put(y ).put(z+w); uv.put(1).put(0);
+ pos.put(x).put(y+h).put(z+w); uv.put(1).put(1);
+ pos.put(x).put(y+h).put(z-w); uv.put(0).put(1);
+
+ idx.put(vi ).put(vi+1).put(vi+2);
+ idx.put(vi ).put(vi+2).put(vi+3);
+ idx.put(vi+4).put(vi+5).put(vi+6);
+ idx.put(vi+4).put(vi+6).put(vi+7);
+ vi += 8;
+ }
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv);
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
+ mesh.updateBound();
+ return mesh;
+ }
+
+ // ── LOD-Control ───────────────────────────────────────────────────────────
+
+ private static final class GrassVisibilityControl extends AbstractControl {
+ private final Camera cam;
+ private final Vector3f center;
+
+ GrassVisibilityControl(Camera cam, Vector3f center) {
+ this.cam = cam;
+ this.center = center;
+ }
+
+ @Override
+ protected void controlUpdate(float tpf) {
+ float distSq = cam.getLocation().distanceSquared(center);
+ spatial.setCullHint(distSq > GRASS_FAR_DIST_SQ
+ ? Spatial.CullHint.Always
+ : Spatial.CullHint.Inherit);
+ }
+
+ @Override protected void controlRender(RenderManager rm, ViewPort vp) {}
+ }
+
+ // ── Hilfsmethoden ─────────────────────────────────────────────────────────
+
+ private void markChunkDirtyAtPixel(int px, int pz) {
+ float worldX = px * SPLAT_WE_PER_PX - TERRAIN_HALF;
+ float worldZ = pz * SPLAT_WE_PER_PX - TERRAIN_HALF;
+ int cx = (int)((worldX + TERRAIN_HALF) / CHUNK_SIZE);
+ int cz = (int)((worldZ + TERRAIN_HALF) / CHUNK_SIZE);
+ if (cx >= 0 && cx < CHUNKS_PER_AXIS && cz >= 0 && cz < CHUNKS_PER_AXIS) {
+ dirtyChunks[cx + cz * CHUNKS_PER_AXIS] = true;
+ }
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java
new file mode 100644
index 0000000..26fa258
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java
@@ -0,0 +1,595 @@
+package de.blight.editor.state;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.collision.CollisionResults;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.*;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.*;
+import com.jme3.scene.shape.Box;
+import com.jme3.scene.shape.Cylinder;
+import com.jme3.scene.shape.Quad;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.texture.Texture;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.export.binary.BinaryExporter;
+import de.blight.editor.SharedInput;
+import de.blight.editor.object.SceneObject;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Verwaltet platzierte Szenen-Objekte im Welt-Editor.
+ *
+ * - Linksklick im Objekt-Modus: selektiert ein vorhandenes Objekt
+ * oder platziert ein neues (wenn pendingModelPath gesetzt).
+ * - Gizmo: 3 Translationspfeile (X/Y/Z) + 1 Rotationsring (Y),
+ * am ausgewählten Objekt angebracht.
+ * - Drag auf Pfeil → translateY/X/Z; Drag auf Ring → rotY.
+ */
+public class SceneObjectState extends BaseAppState {
+
+ private static final Path ASSET_ROOT = Paths.get("editor-assets");
+
+ // ── Gizmo-Farben ─────────────────────────────────────────────────────────
+ private static final ColorRGBA COL_X = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f);
+ private static final ColorRGBA COL_Y = new ColorRGBA(0.1f, 0.9f, 0.1f, 1f);
+ private static final ColorRGBA COL_Z = new ColorRGBA(0.1f, 0.3f, 1.0f, 1f);
+ private static final ColorRGBA COL_ROT = new ColorRGBA(1.0f, 0.7f, 0.0f, 1f);
+
+ private static final float ARROW_LEN = 3.0f;
+ private static final float ARROW_RADIUS = 0.12f;
+ private static final float PX_PER_WE = 0.15f; // Sensitivität Translation
+ private static final float ROT_PER_PX = 0.015f; // Sensitivität Rotation
+
+ // ── Zustand ──────────────────────────────────────────────────────────────
+ private final SharedInput input;
+ private SimpleApplication app;
+ private Camera cam;
+ private AssetManager assets;
+ private Node rootNode;
+ private TerrainQuad terrain;
+
+ private final List objects = new ArrayList<>();
+ private final List objNodes = new ArrayList<>();
+
+ private Node objectRoot; // hält alle Objekt-Nodes
+ private Node gizmoNode; // hält Gizmo-Geometrien
+ private Geometry arrowX, arrowY, arrowZ, ringRot;
+
+ private int selectedIdx = -1;
+ private int activeGizmo = -1; // 0=X,1=Y,2=Z,3=rot; -1=keins
+
+ private Node previewNode;
+ private String previewModelPath; // gecachter Pfad, um Reload zu vermeiden
+
+ // ── Konstruktor ──────────────────────────────────────────────────────────
+
+ public SceneObjectState(SharedInput input) {
+ this.input = input;
+ }
+
+ public void setTerrain(TerrainQuad terrain) {
+ this.terrain = terrain;
+ }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.cam = app.getCamera();
+ this.assets = app.getAssetManager();
+ this.rootNode = this.app.getRootNode();
+
+ objectRoot = new Node("sceneObjects");
+ rootNode.attachChild(objectRoot);
+
+ gizmoNode = new Node("gizmo");
+ buildGizmo();
+ gizmoNode.setCullHint(Spatial.CullHint.Always);
+
+ previewNode = new Node("objectPreview");
+ previewNode.setCullHint(Spatial.CullHint.Always);
+ rootNode.attachChild(previewNode);
+ }
+
+ @Override
+ protected void cleanup(Application app) {
+ objectRoot.removeFromParent();
+ gizmoNode.removeFromParent();
+ previewNode.removeFromParent();
+ }
+
+ @Override protected void onEnable() {}
+ @Override protected void onDisable() {}
+
+ // ── Update ────────────────────────────────────────────────────────────────
+
+ @Override
+ public void update(float tpf) {
+ // Modell-Konvertierung und Mesh-Erstellung (unabhängig vom aktiven Layer)
+ SharedInput.ModelConvertRequest conv;
+ while ((conv = input.modelConvertQueue.poll()) != null) {
+ convertModel(conv);
+ }
+ SharedInput.MeshCreateRequest meshReq;
+ while ((meshReq = input.meshCreateQueue.poll()) != null) {
+ createMesh(meshReq);
+ }
+
+ updatePreview();
+ boolean isObjectLayer = input.activeLayer == SharedInput.LAYER_OBJECTS
+ || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT;
+ if (!isObjectLayer) return;
+
+ // Solid-Flag-Änderung von JavaFX
+ Boolean solidChange = input.pendingSolidChange;
+ if (solidChange != null) {
+ input.pendingSolidChange = null;
+ if (selectedIdx >= 0) objects.get(selectedIdx).solid = solidChange;
+ }
+
+ // Klick-Events
+ SharedInput.ObjectClick click;
+ while ((click = input.objectClickQueue.poll()) != null) {
+ handleClick(click);
+ }
+
+ // Gizmo-Drags
+ SharedInput.ObjectDrag drag;
+ while ((drag = input.objectDragQueue.poll()) != null) {
+ handleGizmoDrag(drag);
+ }
+
+ // Gizmo nachführen
+ if (selectedIdx >= 0) {
+ updateGizmoPosition();
+ }
+ }
+
+ // ── Platzierungs-Vorschau ─────────────────────────────────────────────────
+
+ private void updatePreview() {
+ String modelPath = input.pendingModelPath;
+
+ if (input.activeLayer != SharedInput.LAYER_OBJECTS || modelPath == null
+ || input.mouseScreenX < 0 || terrain == null) {
+ previewNode.setCullHint(Spatial.CullHint.Always);
+ return;
+ }
+
+ if (!modelPath.equals(previewModelPath)) {
+ previewNode.detachAllChildren();
+ previewModelPath = modelPath;
+ try {
+ Spatial model = modelPath.startsWith("@")
+ ? createPrimitiveSpatial(modelPath.substring(1))
+ : assets.loadModel(modelPath);
+ stripControlsRecursive(model);
+ applyPreviewMaterial(model);
+ previewNode.attachChild(model);
+ } catch (Exception e) {
+ previewNode.setCullHint(Spatial.CullHint.Always);
+ return;
+ }
+ }
+
+ float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
+ float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
+ Ray ray = screenToRay(jmeX, jmeY);
+
+ CollisionResults hits = new CollisionResults();
+ terrain.collideWith(ray, hits);
+ if (hits.size() == 0) {
+ previewNode.setCullHint(Spatial.CullHint.Always);
+ return;
+ }
+
+ Vector3f pt = hits.getClosestCollision().getContactPoint();
+ previewNode.setLocalTranslation(pt.x, pt.y, pt.z);
+ previewNode.setCullHint(Spatial.CullHint.Inherit);
+ }
+
+ private void applyPreviewMaterial(Spatial s) {
+ if (s instanceof Geometry geo) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.3f, 0.8f, 1.0f, 1f));
+ mat.getAdditionalRenderState().setWireframe(true);
+ mat.getAdditionalRenderState().setDepthTest(false);
+ geo.setMaterial(mat);
+ geo.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
+ } else if (s instanceof Node n) {
+ for (Spatial child : new java.util.ArrayList<>(n.getChildren())) {
+ applyPreviewMaterial(child);
+ }
+ }
+ }
+
+ // ── Klick-Handling ────────────────────────────────────────────────────────
+
+ private void handleClick(SharedInput.ObjectClick click) {
+ if (click.rightButton()) return; // Rechtsklick reserviert für Kamera
+
+ float jmeX = (float)(click.screenX() * input.viewportScaleX);
+ float jmeY = cam.getHeight() - (float)(click.screenY() * input.viewportScaleY);
+
+ Ray ray = screenToRay(jmeX, jmeY);
+
+ // 1. Gizmo-Test (Priorität)
+ if (selectedIdx >= 0) {
+ int hit = pickGizmo(ray);
+ if (hit >= 0) { activeGizmo = hit; return; }
+ }
+ activeGizmo = -1;
+
+ // 2. Objekt-Treffer?
+ CollisionResults objHits = new CollisionResults();
+ objectRoot.collideWith(ray, objHits);
+ if (objHits.size() > 0) {
+ Spatial hit = objHits.getClosestCollision().getGeometry();
+ int idx = findObjectIndexByNode(hit);
+ if (idx >= 0) { selectObject(idx); return; }
+ }
+
+ // 3. Terrain-Treffer – Platzieren nur im Platzieren-Modus
+ if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; }
+
+ String modelPath = input.pendingModelPath;
+ if (terrain == null) return;
+ CollisionResults terrHits = new CollisionResults();
+ terrain.collideWith(ray, terrHits);
+ if (terrHits.size() == 0) return;
+
+ if (modelPath == null) { deselectAll(); return; }
+
+ Vector3f pt = terrHits.getClosestCollision().getContactPoint();
+ previewNode.setCullHint(Spatial.CullHint.Always);
+ placeObject(modelPath, pt.x, pt.z, pt.y);
+ }
+
+ // ── Objekt platzieren ────────────────────────────────────────────────────
+
+ private void placeObject(String modelPath, float wx, float wz, float wy) {
+ SceneObject so = new SceneObject(modelPath, wx, wz, wy, false);
+ objects.add(so);
+
+ Node node = loadModelNode(modelPath, wx, wy, wz);
+ objNodes.add(node);
+ objectRoot.attachChild(node);
+
+ selectObject(objects.size() - 1);
+ input.objectJustPlaced = true;
+ setStatus("Platziert: " + modelPath);
+ }
+
+ private Node loadModelNode(String modelPath, float wx, float wy, float wz) {
+ Node node = new Node("obj_" + objects.size());
+ try {
+ Spatial model = modelPath.startsWith("@")
+ ? createPrimitiveSpatial(modelPath.substring(1))
+ : assets.loadModel(modelPath);
+ if (!modelPath.startsWith("@")) stripControlsRecursive(model);
+ if (modelPath.startsWith("@")) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.75f, 0.75f, 0.75f, 1f));
+ if (model instanceof Geometry g) g.setMaterial(mat);
+ }
+ node.attachChild(model);
+ } catch (Exception e) {
+ Geometry box = new Geometry("placeholder", new Box(0.5f, 0.5f, 0.5f));
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", ColorRGBA.Red);
+ box.setMaterial(mat);
+ node.attachChild(box);
+ }
+ node.setLocalTranslation(wx, wy, wz);
+ return node;
+ }
+
+ private Spatial createPrimitiveSpatial(String type) {
+ return switch (type) {
+ case "sphere" -> new Geometry("Kugel", new Sphere(24, 24, 1f));
+ case "cylinder" -> new Geometry("Zylinder", new Cylinder(2, 24, 0.5f, 2f, true));
+ case "plane" -> {
+ Geometry g = new Geometry("Ebene", new Quad(2f, 2f));
+ g.rotate(-FastMath.HALF_PI, 0, 0);
+ g.setLocalTranslation(-1f, 0, 1f);
+ yield g;
+ }
+ default -> new Geometry("Box", new Box(0.5f, 0.5f, 0.5f));
+ };
+ }
+
+ /**
+ * Entfernt alle Controls (auch null-Einträge aus fehlgeschlagener Deserialisierung)
+ * rekursiv aus dem Szene-Graphen. Nötig, weil TreeLodControl keinen no-arg
+ * Konstruktor hat und als null im controls-Array zurückbleibt.
+ */
+ private static void stripControlsRecursive(Spatial s) {
+ try {
+ java.lang.reflect.Field f = com.jme3.scene.Spatial.class.getDeclaredField("controls");
+ f.setAccessible(true);
+ ((java.util.List>) f.get(s)).clear();
+ } catch (Exception ignored) {}
+ if (s instanceof Node n) {
+ for (Spatial child : new java.util.ArrayList<>(n.getChildren())) {
+ stripControlsRecursive(child);
+ }
+ }
+ }
+
+ // ── Selektion ─────────────────────────────────────────────────────────────
+
+ private void selectObject(int idx) {
+ selectedIdx = idx;
+ SceneObject so = objects.get(idx);
+
+ gizmoNode.removeFromParent();
+ objNodes.get(idx).attachChild(gizmoNode);
+ gizmoNode.setLocalTranslation(0, 0, 0);
+ gizmoNode.setCullHint(Spatial.CullHint.Inherit);
+
+ // Info für JavaFX serialisieren
+ input.selectedObjectInfo = so.modelPath + "|" + so.solid + "|"
+ + so.getWorldX() + "|" + so.getGroundY() + "|" + so.getWorldZ()
+ + "|" + so.getRotY() + "|" + so.getScale();
+ input.objectSelectionChanged = true;
+ }
+
+ private void deselectAll() {
+ selectedIdx = -1;
+ activeGizmo = -1;
+ gizmoNode.removeFromParent();
+ gizmoNode.setCullHint(Spatial.CullHint.Always);
+ input.selectedObjectInfo = null;
+ input.objectSelectionChanged = true;
+ }
+
+ // ── Gizmo-Drag ───────────────────────────────────────────────────────────
+
+ private void handleGizmoDrag(SharedInput.ObjectDrag drag) {
+ if (selectedIdx < 0 || activeGizmo < 0) return;
+ SceneObject so = objects.get(selectedIdx);
+ Node node = objNodes.get(selectedIdx);
+
+ // dx = horizontale Mausbewegung, dy = vertikale (positiv = nach unten)
+ float dx = drag.dx() * PX_PER_WE;
+ float dy = drag.dy() * PX_PER_WE;
+
+ switch (activeGizmo) {
+ case 0 -> { so.translate(dx, 0, 0); node.move(dx, 0, 0); } // X
+ case 1 -> { so.translate(0, -dy, 0); node.move(0, -dy, 0); } // Y (oben = negatives dy)
+ case 2 -> { so.translate(0, 0, dx); node.move(0, 0, dx); } // Z
+ case 3 -> {
+ float rad = drag.dx() * ROT_PER_PX;
+ so.rotateY(rad);
+ node.rotate(0, rad, 0);
+ }
+ }
+
+ updateGizmoPosition();
+
+ input.selectedObjectInfo = so.modelPath + "|" + so.solid + "|"
+ + so.getWorldX() + "|" + so.getGroundY() + "|" + so.getWorldZ()
+ + "|" + so.getRotY() + "|" + so.getScale();
+ input.objectSelectionChanged = true;
+ }
+
+ // ── Gizmo-Bau ────────────────────────────────────────────────────────────
+
+ private void buildGizmo() {
+ arrowX = makeArrow(COL_X);
+ arrowY = makeArrow(COL_Y);
+ arrowZ = makeArrow(COL_Z);
+ ringRot = makeRing(COL_ROT);
+
+ // X-Pfeil: entlang +X
+ arrowX.setLocalRotation(new Quaternion().fromAngleAxis(
+ -FastMath.HALF_PI, Vector3f.UNIT_Z));
+ arrowX.setLocalTranslation(ARROW_LEN * 0.5f, 0, 0);
+
+ // Y-Pfeil: entlang +Y (Standard)
+ arrowY.setLocalTranslation(0, ARROW_LEN * 0.5f, 0);
+
+ // Z-Pfeil: entlang +Z
+ arrowZ.setLocalRotation(new Quaternion().fromAngleAxis(
+ FastMath.HALF_PI, Vector3f.UNIT_X));
+ arrowZ.setLocalTranslation(0, 0, ARROW_LEN * 0.5f);
+
+ // Rotationsring: horizontal (XZ-Ebene), leicht oberhalb
+ ringRot.setLocalTranslation(0, ARROW_LEN + 0.3f, 0);
+
+ gizmoNode.attachChild(arrowX);
+ gizmoNode.attachChild(arrowY);
+ gizmoNode.attachChild(arrowZ);
+ gizmoNode.attachChild(ringRot);
+ }
+
+ private Geometry makeArrow(ColorRGBA color) {
+ Cylinder cyl = new Cylinder(2, 6, ARROW_RADIUS, ARROW_LEN, true);
+ Geometry g = new Geometry("gizmoArrow", cyl);
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color);
+ mat.getAdditionalRenderState().setDepthTest(false);
+ g.setMaterial(mat);
+ g.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
+ return g;
+ }
+
+ private Geometry makeRing(ColorRGBA color) {
+ // Einfacher dünner Torus-Ersatz: Kreis aus Liniensegmenten
+ int segs = 24;
+ float r = ARROW_LEN * 0.7f;
+ com.jme3.scene.shape.Torus torus =
+ new com.jme3.scene.shape.Torus(segs, 6, ARROW_RADIUS, r);
+ Geometry g = new Geometry("gizmoRing", torus);
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color);
+ mat.getAdditionalRenderState().setDepthTest(false);
+ g.setMaterial(mat);
+ g.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
+ return g;
+ }
+
+ private void updateGizmoPosition() {
+ // Gizmo ist Kind des Objekt-Nodes → lokal (0,0,0) ist am Objekt-Ursprung.
+ // Skalierung: unveränderliche Pixelgröße durch Abstandsskalierung.
+ if (selectedIdx < 0) return;
+ Node objNode = objNodes.get(selectedIdx);
+ float dist = cam.getLocation().distance(objNode.getWorldTranslation());
+ float scale = Math.max(1f, dist * 0.1f);
+ gizmoNode.setLocalScale(scale);
+ }
+
+ // ── Gizmo-Picking ─────────────────────────────────────────────────────────
+
+ private int pickGizmo(Ray ray) {
+ CollisionResults hits = new CollisionResults();
+ arrowX.collideWith(ray, hits);
+ if (hits.size() > 0) return 0;
+ hits.clear();
+ arrowY.collideWith(ray, hits);
+ if (hits.size() > 0) return 1;
+ hits.clear();
+ arrowZ.collideWith(ray, hits);
+ if (hits.size() > 0) return 2;
+ hits.clear();
+ ringRot.collideWith(ray, hits);
+ if (hits.size() > 0) return 3;
+ return -1;
+ }
+
+ // ── Hilfsmethoden ────────────────────────────────────────────────────────
+
+ private Ray screenToRay(float jmeX, float jmeY) {
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
+ Vector3f dir = far.subtract(near).normalizeLocal();
+ return new Ray(near, dir);
+ }
+
+ private int findObjectIndexByNode(Spatial hit) {
+ for (int i = 0; i < objNodes.size(); i++) {
+ if (isDescendantOf(hit, objNodes.get(i))) return i;
+ }
+ return -1;
+ }
+
+ private static boolean isDescendantOf(Spatial child, Node ancestor) {
+ Spatial cur = child;
+ while (cur != null) {
+ if (cur == ancestor) return true;
+ cur = cur.getParent();
+ }
+ return false;
+ }
+
+ // ── Mesh-Erstellung ───────────────────────────────────────────────────────
+
+ private void createMesh(SharedInput.MeshCreateRequest req) {
+ Mesh mesh = switch (req.form()) {
+ case "Kugel" -> new Sphere(32, 32, req.sizeX());
+ case "Zylinder" -> new Cylinder(2, 32, req.sizeX(), req.sizeY(), true);
+ case "Ebene" -> new Quad(req.sizeX(), req.sizeZ());
+ default -> new Box(req.sizeX() * 0.5f, req.sizeY() * 0.5f, req.sizeZ() * 0.5f);
+ };
+
+ Geometry geo = new Geometry(req.name(), mesh);
+
+ // Ebene horizontal ausrichten (XZ-Ebene)
+ if ("Ebene".equals(req.form())) {
+ geo.rotate(-FastMath.HALF_PI, 0, 0);
+ geo.setLocalTranslation(-req.sizeX() * 0.5f, 0, req.sizeZ() * 0.5f);
+ }
+
+ geo.setMaterial(buildMeshMaterial(req));
+ if (req.a() < 1f) geo.setQueueBucket(RenderQueue.Bucket.Transparent);
+
+ Node wrapper = new Node(req.name());
+ wrapper.attachChild(geo);
+
+ try {
+ Path destDir = ASSET_ROOT.resolve("models");
+ Files.createDirectories(destDir);
+ Path dest = destDir.resolve(req.name() + ".j3o");
+ BinaryExporter.getInstance().save(wrapper, dest.toFile());
+ setStatus("Mesh gespeichert: " + req.name() + ".j3o");
+ input.refreshAssets = true;
+ } catch (IOException e) {
+ setStatus("Fehler beim Speichern des Meshes: " + e.getMessage());
+ }
+ }
+
+ private Material buildMeshMaterial(SharedInput.MeshCreateRequest req) {
+ ColorRGBA color = new ColorRGBA(req.r(), req.g(), req.b(), req.a());
+ Material mat;
+
+ if ("Phong".equals(req.matType())) {
+ mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
+ mat.setColor("Diffuse", color);
+ mat.setColor("Ambient", new ColorRGBA(req.r() * 0.3f, req.g() * 0.3f, req.b() * 0.3f, req.a()));
+ mat.setColor("Specular", ColorRGBA.White);
+ mat.setFloat("Shininess", 32f);
+ mat.setBoolean("UseMaterialColors", true);
+ if (req.texturePath() != null) {
+ try {
+ Texture tex = assets.loadTexture(req.texturePath());
+ tex.setWrap(Texture.WrapMode.Repeat);
+ mat.setTexture("DiffuseMap", tex);
+ } catch (Exception e) {
+ setStatus("Textur nicht geladen (" + req.texturePath() + "): " + e.getMessage());
+ }
+ }
+ } else {
+ mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color);
+ if (req.texturePath() != null) {
+ try {
+ Texture tex = assets.loadTexture(req.texturePath());
+ tex.setWrap(Texture.WrapMode.Repeat);
+ mat.setTexture("ColorMap", tex);
+ } catch (Exception e) {
+ setStatus("Textur nicht geladen (" + req.texturePath() + "): " + e.getMessage());
+ }
+ }
+ }
+
+ if (req.wireframe()) mat.getAdditionalRenderState().setWireframe(true);
+ if (req.a() < 1f) mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+
+ return mat;
+ }
+
+ // ── Modell-Konvertierung ──────────────────────────────────────────────────
+
+ private void convertModel(SharedInput.ModelConvertRequest req) {
+ setStatus("Konvertiere " + req.assetPath() + " …");
+ try {
+ Spatial model = assets.loadModel(req.assetPath());
+ stripControlsRecursive(model);
+ Files.createDirectories(req.destJ3o().getParent());
+ BinaryExporter.getInstance().save(model, req.destJ3o().toFile());
+ if (req.srcToDelete() != null) Files.deleteIfExists(req.srcToDelete());
+ setStatus("Konvertiert: " + req.destJ3o().getFileName());
+ input.refreshAssets = true;
+ } catch (Exception e) {
+ setStatus("Konvertierung fehlgeschlagen (" + req.assetPath() + "): " + e.getMessage());
+ System.err.println("[SceneObject] Konvertierung fehlgeschlagen: " + e.getMessage());
+ }
+ }
+
+ private void setStatus(String msg) {
+ input.treeGenStatusMsg = msg; // recycled volatile field für Statuszeile
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.class b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.class
new file mode 100644
index 0000000..57c15fe
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java
index 6ce71f5..f4d2c6f 100644
--- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java
+++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java
@@ -15,44 +15,75 @@ import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Quad;
+import com.jme3.terrain.geomipmap.TerrainLodControl;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
+import com.jme3.util.SkyFactory;
+import de.blight.common.MapData;
+import de.blight.common.MapIO;
import de.blight.editor.SharedInput;
+import de.blight.editor.tool.HeightTool;
+import java.io.IOException;
+import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
public class TerrainEditorState extends BaseAppState {
- // ── Konstanten ──────────────────────────────────────────────────────────
- private static final int V = 17; // Vertices pro Achse (16 Zellen)
- private static final float BRUSH_RADIUS = 2.0f; // Meter
- private static final float BRUSH_DELTA = 0.25f; // Höhenänderung pro Klick
- private static final float CAM_SPEED = 12f;
- private static final float MOUSE_SENS = 0.003f;
+ // ── Terrain-Konstanten ────────────────────────────────────────────────────
+ private static final int TERRAIN_SIZE = 4096;
+ private static final int TOTAL_SIZE = TERRAIN_SIZE + 1; // 4097
+ private static final int PATCH_SIZE = 65;
- // ── Zustand ─────────────────────────────────────────────────────────────
+ // ── Splatmap-Konstanten ────────────────────────────────────────────────────
+ private static final int SPLAT_SIZE = MapData.SPLAT_SIZE; // 513
+ private static final float WORLD_HALF = 2048f;
+ private static final float SPLAT_WE_PER_PX = 4096f / (SPLAT_SIZE - 1); // 8 WE/px
+
+ // ── Kamera ────────────────────────────────────────────────────────────────
+ private static final float CAM_SPEED = 300f;
+ private static final float ORBIT_SPEED = 1.5f;
+ private static final float MOUSE_SENS = 0.003f;
+
+ // ── Zustand ──────────────────────────────────────────────────────────────
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private Node rootNode;
private final SharedInput input;
- private final float[] heights = new float[V * V]; // flaches Array, Index = z*V+x
- private Mesh terrainMesh;
- private Geometry terrainGeo;
+ private TerrainQuad terrain;
+ private Geometry brushIndicator;
+ private UpperLayerState upperLayerState;
+ private PlacedObjectState placedObjectState;
+ private MapData loadedMapData;
- // Kamera-Euler-Winkel
- private float camYaw = 0f;
- private float camPitch = -0.4f;
- private final Vector3f camPos = new Vector3f(0, 14, 22);
+ // ── Splatmap ─────────────────────────────────────────────────────────────
+ private byte[] splatR, splatG, splatB;
+ private ByteBuffer splatBuf;
+ private Image splatImage;
+ private Texture2D splatTex;
+
+ // ── Kameraposition ────────────────────────────────────────────────────────
+ private float camYaw = 0f;
+ private float camPitch = -1.0f;
+ private final Vector3f camPos = new Vector3f(0f, 800f, (float)(800.0 / Math.tan(1.0)));
public TerrainEditorState(SharedInput input) {
this.input = input;
}
- // ── Lifecycle ────────────────────────────────────────────────────────────
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
@@ -60,6 +91,16 @@ public class TerrainEditorState extends BaseAppState {
this.cam = app.getCamera();
this.assets = app.getAssetManager();
this.rootNode = this.app.getRootNode();
+ cam.setFrustumFar(8000f);
+
+ if (MapIO.exists()) {
+ try {
+ loadedMapData = MapIO.load();
+ System.out.println("[TerrainEditor] Karte geladen: " + MapIO.getMapPath());
+ } catch (IOException e) {
+ System.err.println("[TerrainEditor] Karte nicht ladbar: " + e.getMessage());
+ }
+ }
}
@Override
@@ -75,115 +116,505 @@ public class TerrainEditorState extends BaseAppState {
@Override protected void cleanup(Application app) {}
- // ── Szene aufbauen ───────────────────────────────────────────────────────
+ // ── Szene aufbauen ────────────────────────────────────────────────────────
private void buildScene() {
- // Licht
DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
sun.setColor(new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
rootNode.addLight(sun);
+ DirectionalLight topLight = new DirectionalLight();
+ topLight.setDirection(Vector3f.UNIT_Y.negate());
+ topLight.setColor(new ColorRGBA(0.8f, 0.8f, 0.8f, 1f));
+ rootNode.addLight(topLight);
+
AmbientLight ambient = new AmbientLight(new ColorRGBA(0.35f, 0.38f, 0.45f, 1f));
rootNode.addLight(ambient);
- // Terrain
- terrainGeo = buildTerrainGeometry();
- rootNode.attachChild(terrainGeo);
+ terrain = buildTerrain();
+ rootNode.attachChild(terrain);
+
+ upperLayerState = new UpperLayerState(input, loadedMapData);
+ app.getStateManager().attach(upperLayerState);
+
+ placedObjectState = new PlacedObjectState(input, loadedMapData);
+ placedObjectState.setTerrain(terrain);
+ app.getStateManager().attach(placedObjectState);
+
+ SceneObjectState sceneObjState = app.getStateManager().getState(SceneObjectState.class);
+ if (sceneObjState != null) sceneObjState.setTerrain(terrain);
- // Wasser bei Y = 0
rootNode.attachChild(buildWater());
+ rootNode.attachChild(buildGrid());
- // Raster-Linien auf dem Terrain (als dünne Linien-Node wäre komplex, Gitter via Grid-Overlay)
- // Einfache Grid-Markierung: ein flaches transparentes Quad mit Wireframe
- rootNode.attachChild(buildGridOverlay());
+ brushIndicator = buildBrushIndicator();
+ rootNode.attachChild(brushIndicator);
- // Himmel (einfacher Hintergrund-Farbverlauf über Viewport-BG-Farbe)
- app.getViewPort().setBackgroundColor(new ColorRGBA(0.45f, 0.60f, 0.80f, 1f));
+ try {
+ rootNode.attachChild(SkyFactory.createSky(assets,
+ "Textures/Sky/Bright/BrightSky.dds", SkyFactory.EnvMapType.CubeMap));
+ } catch (Exception e) {
+ app.getViewPort().setBackgroundColor(new ColorRGBA(0.45f, 0.60f, 0.80f, 1f));
+ }
}
- private Geometry buildTerrainGeometry() {
- terrainMesh = new Mesh();
- FloatBuffer posBuf = BufferUtils.createFloatBuffer(V * V * 3);
- FloatBuffer normBuf = BufferUtils.createFloatBuffer(V * V * 3);
- FloatBuffer texBuf = BufferUtils.createFloatBuffer(V * V * 2);
- IntBuffer idxBuf = BufferUtils.createIntBuffer((V - 1) * (V - 1) * 6);
+ // ── Terrain ───────────────────────────────────────────────────────────────
- for (int z = 0; z < V; z++) {
- for (int x = 0; x < V; x++) {
- posBuf.put(x).put(heights[z * V + x]).put(z);
- normBuf.put(0).put(1).put(0);
- texBuf.put(x / 16f).put(z / 16f);
- }
- }
- for (int z = 0; z < V - 1; z++) {
- for (int x = 0; x < V - 1; x++) {
- int bl = z * V + x, br = bl + 1;
- int tl = bl + V, tr = tl + 1;
- idxBuf.put(bl).put(tr).put(br);
- idxBuf.put(bl).put(tl).put(tr);
- }
+ private TerrainQuad buildTerrain() {
+ float[] heights;
+ if (loadedMapData != null) {
+ heights = loadedMapData.terrainHeight;
+ } else {
+ heights = new float[TOTAL_SIZE * TOTAL_SIZE];
+ Arrays.fill(heights, 1f);
}
- terrainMesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
- terrainMesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
- terrainMesh.setBuffer(VertexBuffer.Type.TexCoord, 2, texBuf);
- terrainMesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
- terrainMesh.updateBound();
+ TerrainQuad tq = new TerrainQuad("terrain", PATCH_SIZE, TOTAL_SIZE, heights);
+ // Kein scaleTerrainUVs – Terrain.j3md nutzt TexNScale direkt
- Geometry geo = new Geometry("terrain", terrainMesh);
- geo.setLocalTranslation(-8, 0, -8); // Terrain zentriert bei Ursprung
+ TerrainLodControl lod = new TerrainLodControl(tq, cam);
+ tq.addControl(lod);
- Material mat = new Material(assets, "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.12f, 0.28f, 0.08f, 1f));
- mat.setColor("Specular", ColorRGBA.Black);
- mat.setFloat("Shininess", 0f);
- geo.setMaterial(mat);
- return geo;
+ initSplatmap();
+ tq.setMaterial(buildTerrainMaterial());
+ return tq;
}
- private Geometry buildWater() {
- Geometry water = new Geometry("water", new Quad(16, 16));
- water.rotate(-FastMath.HALF_PI, 0, 0);
- water.setLocalTranslation(-8, 0.01f, 8); // leicht über Y=0 damit kein Z-Fighting
+ private void initSplatmap() {
+ if (loadedMapData != null) {
+ splatR = loadedMapData.splatR.clone();
+ splatG = loadedMapData.splatG.clone();
+ splatB = loadedMapData.splatB.clone();
+ // Ältere Maps haben splatR noch auf 0 (altes falsches Mapping).
+ // Alpha.R=0 → tex1*0=schwarz. Wenn R überall Null, auf 255 (=Gras) initialisieren.
+ boolean rAllZero = true;
+ for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } }
+ if (rAllZero) Arrays.fill(splatR, (byte) 255);
+ } else {
+ splatR = new byte[SPLAT_SIZE * SPLAT_SIZE];
+ splatG = new byte[SPLAT_SIZE * SPLAT_SIZE];
+ splatB = new byte[SPLAT_SIZE * SPLAT_SIZE];
+ Arrays.fill(splatR, (byte) 255); // R=1 → Tex1 (Gras) überall sichtbar
+ }
- Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", new ColorRGBA(0.05f, 0.25f, 0.70f, 0.55f));
- mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- water.setQueueBucket(RenderQueue.Bucket.Transparent);
- water.setMaterial(mat);
- return water;
+ splatBuf = BufferUtils.createByteBuffer(SPLAT_SIZE * SPLAT_SIZE * 4);
+ for (int i = 0; i < SPLAT_SIZE * SPLAT_SIZE; i++) {
+ splatBuf.put(splatR[i]);
+ splatBuf.put(splatG[i]);
+ splatBuf.put(splatB[i]);
+ splatBuf.put((byte) 0);
+ }
+ splatBuf.flip();
+
+ splatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, splatBuf);
+ splatTex = new Texture2D(splatImage);
+ splatTex.setWrap(Texture.WrapMode.EdgeClamp);
+ splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
+ splatTex.setMagFilter(Texture.MagFilter.Bilinear);
}
- private Geometry buildGridOverlay() {
- // Einfaches Wireframe-Duplikat des Terrains als Gitter
- Geometry grid = new Geometry("grid", terrainMesh);
- grid.setLocalTranslation(-8, 0.02f, -8);
+ private Material buildTerrainMaterial() {
+ Material mat = new Material(assets, "Common/MatDefs/Terrain/Terrain.j3md");
- Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0.25f));
- mat.getAdditionalRenderState().setWireframe(true);
- mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- grid.setQueueBucket(RenderQueue.Bucket.Transparent);
- grid.setMaterial(mat);
- return grid;
+ // Terrain.j3md Shader: outColor = tex1*alpha.r → mix(tex2,g) → mix(tex3,b)
+ // d.h. Alpha.R = Tex1-Helligkeit (immer 1), Alpha.G = Tex2-Blend, Alpha.B = Tex3-Blend
+ Texture tex1 = loadOrFallback("Textures/Terrain/splat/grass.jpg",
+ new ColorRGBA(0.28f, 0.58f, 0.18f, 1f));
+ Texture tex2 = loadOrFallback("Textures/Terrain/splat/road.jpg",
+ new ColorRGBA(0.55f, 0.50f, 0.40f, 1f));
+ Texture tex3 = loadOrFallback("Textures/Terrain/splat/Gravel.jpg",
+ new ColorRGBA(0.45f, 0.35f, 0.25f, 1f));
+
+ for (Texture t : List.of(tex1, tex2, tex3)) {
+ t.setWrap(Texture.WrapMode.Repeat);
+ }
+
+ // Skalierung: 512 Kacheln über 4096 WE = 1 Kachel pro 8 WE (Zellgröße)
+ mat.setTexture("Tex1", tex1); mat.setFloat("Tex1Scale", 512f);
+ mat.setTexture("Tex2", tex2); mat.setFloat("Tex2Scale", 512f);
+ mat.setTexture("Tex3", tex3); mat.setFloat("Tex3Scale", 512f);
+ mat.setTexture("Alpha", splatTex);
+
+ return mat;
}
- // ── Update-Schleife ──────────────────────────────────────────────────────
+ private Texture loadOrFallback(String path, ColorRGBA color) {
+ try {
+ return assets.loadTexture(path);
+ } catch (Exception e) {
+ ByteBuffer buf = BufferUtils.createByteBuffer(4);
+ buf.put((byte)(color.r * 255));
+ buf.put((byte)(color.g * 255));
+ buf.put((byte)(color.b * 255));
+ buf.put((byte)(color.a * 255));
+ buf.flip();
+ return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf));
+ }
+ }
+
+ // ── Splatmap malen ────────────────────────────────────────────────────────
+
+ /**
+ * Malt Textur-Gewichte auf die Splatmap.
+ * Shader-Mapping: Alpha.R=Tex1-Helligkeit (fest 1), Alpha.G=Tex2(Fels)-Blend, Alpha.B=Tex3(Erde)-Blend.
+ * @param textureIndex 0=Gras(Reset: G→0,B→0), 1=Fels(G→1,B→0), 2=Erde(G→0,B→1)
+ */
+ private void applyTexturePaint(Vector3f contact, float strength, int textureIndex) {
+ float radius = (float) input.textureTool.brushRadius.getValue();
+
+ int centerPX = Math.round((contact.x + WORLD_HALF) / SPLAT_WE_PER_PX);
+ int centerPZ = Math.round((contact.z + WORLD_HALF) / SPLAT_WE_PER_PX);
+ int pixR = (int) Math.ceil(radius / SPLAT_WE_PER_PX);
+
+ boolean changed = false;
+ for (int dz = -pixR; dz <= pixR; dz++) {
+ int pz = centerPZ + dz;
+ if (pz < 0 || pz >= SPLAT_SIZE) continue;
+ for (int dx = -pixR; dx <= pixR; dx++) {
+ int px = centerPX + dx;
+ if (px < 0 || px >= SPLAT_SIZE) continue;
+
+ float distWE = FastMath.sqrt(dx * dx + dz * dz) * SPLAT_WE_PER_PX;
+ if (distWE >= radius) continue;
+
+ float t = distWE / radius;
+ float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
+ float blend = strength * falloff;
+
+ int idx = pz * SPLAT_SIZE + px;
+ // R bleibt immer 1 (tex1*R = tex1); G und B sind unabhängige mix()-Faktoren
+ float curG = (splatG[idx] & 0xFF) / 255f;
+ float curB = (splatB[idx] & 0xFF) / 255f;
+
+ // Zielwerte je Textur
+ float tgG = (textureIndex == 1) ? 1f : 0f; // Fels: G→1
+ float tgB = (textureIndex == 2) ? 1f : 0f; // Erde: B→1
+ // textureIndex==0 (Gras/Reset): beide Ziele 0
+
+ float newG = curG + (tgG - curG) * blend;
+ float newB = curB + (tgB - curB) * blend;
+
+ splatR[idx] = (byte) 255; // R immer voll
+ splatG[idx] = (byte) Math.round(newG * 255f);
+ splatB[idx] = (byte) Math.round(newB * 255f);
+
+ int bi = idx * 4;
+ splatBuf.put(bi, splatR[idx]);
+ splatBuf.put(bi + 1, splatG[idx]);
+ splatBuf.put(bi + 2, splatB[idx]);
+ changed = true;
+ }
+ }
+ if (changed) {
+ splatBuf.rewind();
+ splatImage.setUpdateNeeded();
+ }
+ }
+
+ // ── Update-Schleife ───────────────────────────────────────────────────────
@Override
public void update(float tpf) {
updateCamera(tpf);
processEdits();
+ processUpperLayerEdits();
+ processTextureEdits();
+ updateBrushIndicator();
+
+ if (input.saveRequested) {
+ input.saveRequested = false;
+ performSave();
+ }
}
+ private static final int MAX_EDITS_PER_FRAME = 2;
+
+ private void processTextureEdits() {
+ SharedInput.TextureEdit edit;
+ int processed = 0;
+ while ((edit = input.textureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
+ processed++;
+ float jmeX = (float)(edit.screenX() * input.viewportScaleX);
+ float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
+
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
+ com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
+
+ CollisionResults hits = new CollisionResults();
+ terrain.collideWith(ray, hits);
+ if (hits.size() == 0) continue;
+
+ Vector3f contact = hits.getClosestCollision().getContactPoint();
+ int texIdx = (edit.action() > 0)
+ ? input.textureTool.textureIndex.getSelectedIndex()
+ : 0; // Rechtsklick = Gras (zurücksetzen)
+ applyTexturePaint(contact, (float) input.textureTool.brushStrength.getValue(), texIdx);
+ }
+ }
+
+ // ── Speichern ─────────────────────────────────────────────────────────────
+
+ private void performSave() {
+ try {
+ MapData data = new MapData();
+
+ float[] hmap = terrain.getHeightMap();
+ if (hmap != null) {
+ System.arraycopy(hmap, 0, data.terrainHeight, 0,
+ Math.min(hmap.length, data.terrainHeight.length));
+ }
+
+ if (upperLayerState != null) {
+ UpperLayerData ud = upperLayerState.data;
+ System.arraycopy(ud.topHeight, 0, data.upperTop, 0, data.upperTop.length);
+ System.arraycopy(ud.bottomHeight, 0, data.upperBottom, 0, data.upperBottom.length);
+ for (int i = 0; i < data.upperHole.length; i++) {
+ data.upperHole[i] = ud.hole[i] ? (byte) 1 : (byte) 0;
+ }
+ }
+
+ if (splatR != null) {
+ System.arraycopy(splatR, 0, data.splatR, 0, data.splatR.length);
+ System.arraycopy(splatG, 0, data.splatG, 0, data.splatG.length);
+ System.arraycopy(splatB, 0, data.splatB, 0, data.splatB.length);
+ }
+
+ if (placedObjectState != null) {
+ System.arraycopy(placedObjectState.getDensityMap(), 0,
+ data.grassDensity, 0, data.grassDensity.length);
+ }
+
+ MapIO.save(data);
+ input.saveStatusMsg = "Gespeichert: " + MapIO.getMapPath();
+ System.out.println("[TerrainEditor] " + input.saveStatusMsg);
+ } catch (IOException e) {
+ input.saveStatusMsg = "Fehler beim Speichern: " + e.getMessage();
+ System.err.println("[TerrainEditor] " + input.saveStatusMsg);
+ }
+ }
+
+ // ── Brush-Indikator ───────────────────────────────────────────────────────
+
+ private void updateBrushIndicator() {
+ float mx = input.mouseScreenX;
+ float my = input.mouseScreenY;
+ if (mx < 0) {
+ brushIndicator.setCullHint(Spatial.CullHint.Always);
+ return;
+ }
+ float jmeX = mx * (float) input.viewportScaleX;
+ float jmeY = cam.getHeight() - my * (float) input.viewportScaleY;
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
+ com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
+
+ Vector3f contactPoint = null;
+ float brushRadius = 0f;
+
+ int layer = input.activeLayer;
+ if (layer == 0 || layer == 4) {
+ CollisionResults hits = new CollisionResults();
+ terrain.collideWith(ray, hits);
+ if (hits.size() > 0) {
+ contactPoint = hits.getClosestCollision().getContactPoint();
+ brushRadius = (layer == 0)
+ ? (float) input.heightTool.brushRadius.getValue()
+ : (float) input.textureTool.brushRadius.getValue();
+ }
+ } else {
+ CollisionResults hits = new CollisionResults();
+ if (upperLayerState != null && input.upperLayerVisible) {
+ upperLayerState.getUpperNode().collideWith(ray, hits);
+ }
+ if (hits.size() > 0) {
+ contactPoint = hits.getClosestCollision().getContactPoint();
+ } else {
+ CollisionResults terrainHits = new CollisionResults();
+ terrain.collideWith(ray, terrainHits);
+ if (terrainHits.size() > 0)
+ contactPoint = terrainHits.getClosestCollision().getContactPoint();
+ }
+ brushRadius = (layer == 1)
+ ? (float) input.upperHeightTool.brushRadius.getValue()
+ : (float) input.holeTool.brushRadius.getValue();
+ }
+
+ if (contactPoint != null) {
+ brushIndicator.setLocalTranslation(contactPoint.x, contactPoint.y + 0.5f, contactPoint.z);
+ brushIndicator.setLocalScale(brushRadius, 1f, brushRadius);
+ brushIndicator.setCullHint(Spatial.CullHint.Inherit);
+ } else {
+ brushIndicator.setCullHint(Spatial.CullHint.Always);
+ }
+ }
+
+ // ── Upper-Layer-Edits ─────────────────────────────────────────────────────
+
+ private void processUpperLayerEdits() {
+ if (upperLayerState == null) return;
+ SharedInput.UpperLayerEdit edit;
+ int processed = 0;
+ while ((edit = input.upperLayerEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
+ processed++;
+ float jmeX = (float)(edit.screenX() * input.viewportScaleX);
+ float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
+
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
+ com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
+
+ CollisionResults hits = new CollisionResults();
+ if (input.upperLayerVisible) {
+ upperLayerState.getUpperNode().collideWith(ray, hits);
+ }
+ if (hits.size() == 0) terrain.collideWith(ray, hits);
+ if (hits.size() == 0) continue;
+
+ Vector3f contact = hits.getClosestCollision().getContactPoint();
+ if (input.activeLayer == 1) {
+ upperLayerState.applyHeightEdit(contact.x, contact.z, edit.action());
+ } else if (input.activeLayer == 2) {
+ upperLayerState.applyHoleEdit(contact.x, contact.z);
+ }
+ }
+ }
+
+ // ── Terrain-Höhen-Edits ───────────────────────────────────────────────────
+
+ private void processEdits() {
+ SharedInput.TerrainEdit edit;
+ int processed = 0;
+ while ((edit = input.editQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
+ processed++;
+ float jmeX = (float)(edit.screenX() * input.viewportScaleX);
+ float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
+
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
+ com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
+
+ CollisionResults hits = new CollisionResults();
+ terrain.collideWith(ray, hits);
+ if (hits.size() == 0) continue;
+
+ Vector3f contact = hits.getClosestCollision().getContactPoint();
+ int mode = input.heightTool.mode.getSelectedIndex();
+ if (mode == HeightTool.MODE_SMOOTH) {
+ smoothHeight(contact);
+ } else {
+ float delta = (float) input.heightTool.brushStrength.getValue() * edit.action();
+ modifyHeight(contact, delta, mode);
+ }
+ }
+ if (processed > 0) terrain.updateModelBound();
+ }
+
+ // ── Höhen-Werkzeug ────────────────────────────────────────────────────────
+
+ private void modifyHeight(Vector3f worldContact, float delta, int mode) {
+ float radius = (float) input.heightTool.brushRadius.getValue();
+ int r = (int) Math.ceil(radius);
+ int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
+ int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
+
+ List locs = new ArrayList<>();
+ List deltas = new ArrayList<>();
+
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ int vx = cx + dx, vz = cz + dz;
+ if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
+
+ float dist = FastMath.sqrt(dx * dx + dz * dz);
+ if (dist >= radius) continue;
+
+ float t = dist / radius;
+ float falloff;
+ switch (mode) {
+ case HeightTool.MODE_SINUS ->
+ falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
+ case HeightTool.MODE_PLATEAU -> {
+ float edge = 0.85f;
+ if (t < edge) {
+ falloff = 1f;
+ } else {
+ float s = (t - edge) / (1f - edge);
+ falloff = 1f - s * s * s * s;
+ }
+ }
+ default -> {
+ float u = 1f - t;
+ falloff = u * u * u * u;
+ }
+ }
+
+ locs.add(new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
+ deltas.add(delta * falloff);
+ }
+ }
+
+ if (!locs.isEmpty()) {
+ terrain.adjustHeight(locs, deltas);
+ if (upperLayerState != null) upperLayerState.adjustHeightsWithTerrain(locs, deltas);
+ if (placedObjectState != null) placedObjectState.adjustObjectHeights(locs, deltas);
+ }
+ }
+
+ private void smoothHeight(Vector3f worldContact) {
+ float radius = (float) input.heightTool.brushRadius.getValue();
+ float strength = (float) input.heightTool.brushStrength.getValue();
+ int r = (int) Math.ceil(radius);
+ int cx = Math.round(worldContact.x + TERRAIN_SIZE * 0.5f);
+ int cz = Math.round(worldContact.z + TERRAIN_SIZE * 0.5f);
+
+ float sum = 0f; int count = 0;
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ int vx = cx + dx, vz = cz + dz;
+ if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
+ if (FastMath.sqrt(dx * dx + dz * dz) >= radius) continue;
+ sum += terrain.getHeightmapHeight(
+ new Vector2f(vx - TERRAIN_SIZE * 0.5f, vz - TERRAIN_SIZE * 0.5f));
+ count++;
+ }
+ }
+ if (count == 0) return;
+ float avg = sum / count;
+
+ List locs = new ArrayList<>();
+ List newHts = new ArrayList<>();
+ List deltas = new ArrayList<>();
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ int vx = cx + dx, vz = cz + dz;
+ if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
+ float dist = FastMath.sqrt(dx * dx + dz * dz);
+ if (dist >= radius) continue;
+ float falloff = 1f - dist / radius;
+ float wx = vx - TERRAIN_SIZE * 0.5f;
+ float wz = vz - TERRAIN_SIZE * 0.5f;
+ float curH = terrain.getHeightmapHeight(new Vector2f(wx, wz));
+ float newH = curH + (avg - curH) * falloff * strength * 3f;
+ locs.add(new Vector2f(wx, wz));
+ newHts.add(newH);
+ deltas.add(newH - curH);
+ }
+ }
+ if (!locs.isEmpty()) {
+ terrain.setHeight(locs, newHts);
+ if (upperLayerState != null) upperLayerState.adjustHeightsWithTerrain(locs, deltas);
+ if (placedObjectState != null) placedObjectState.adjustObjectHeights(locs, deltas);
+ }
+ }
+
+ // ── Kamera ────────────────────────────────────────────────────────────────
+
private void updateCamera(float tpf) {
- // Rotation aus akkumuliertem Maus-Delta
int[] delta = input.consumeMouseDelta();
if (delta[0] != 0 || delta[1] != 0) {
- camYaw -= delta[0] * MOUSE_SENS;
+ camYaw += delta[0] * MOUSE_SENS;
camPitch -= delta[1] * MOUSE_SENS;
camPitch = FastMath.clamp(camPitch,
-FastMath.HALF_PI + 0.05f,
@@ -192,23 +623,41 @@ public class TerrainEditorState extends BaseAppState {
applyCameraTransform();
- // Bewegung in Kamera-Vorwärtsrichtung projiziert auf XZ-Ebene
float speed = CAM_SPEED * tpf;
- Vector3f fwd = cam.getDirection().clone().setY(0);
- if (fwd.lengthSquared() > 0.001f) fwd.normalizeLocal();
- Vector3f lft = cam.getLeft().clone().setY(0);
- if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
+ if (input.forward) camPos.addLocal(cam.getDirection().mult(speed));
+ if (input.backward) camPos.addLocal(cam.getDirection().mult(-speed));
- if (input.forward) camPos.addLocal(fwd.mult(speed));
- if (input.backward) camPos.subtractLocal(fwd.mult(speed));
- if (input.left) camPos.addLocal(lft.mult(speed));
- if (input.right) camPos.subtractLocal(lft.mult(speed));
- if (input.up) camPos.y += speed;
- if (input.down) camPos.y -= speed;
+ Vector3f lft = cam.getLeft().clone().setY(0);
+ if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
+ if (input.left) camPos.addLocal(lft.mult(speed));
+ if (input.right) camPos.subtractLocal(lft.mult(speed));
+
+ if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf);
+ if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf);
cam.setLocation(camPos);
}
+ private void orbitAroundTerrain(float angle) {
+ Vector3f pivot = findTerrainIntersection();
+ if (pivot == null) return;
+ Vector3f offset = camPos.subtract(pivot);
+ Quaternion rot = new Quaternion().fromAngleAxis(angle, Vector3f.UNIT_Y);
+ camPos.set(pivot.add(rot.mult(offset)));
+ camYaw += angle;
+ }
+
+ private Vector3f findTerrainIntersection() {
+ float cx = cam.getWidth() * 0.5f;
+ float cy = cam.getHeight() * 0.5f;
+ Vector3f near = cam.getWorldCoordinates(new Vector2f(cx, cy), 0f);
+ Vector3f far = cam.getWorldCoordinates(new Vector2f(cx, cy), 1f);
+ com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
+ CollisionResults hits = new CollisionResults();
+ terrain.collideWith(ray, hits);
+ return hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null;
+ }
+
private void applyCameraTransform() {
Quaternion yawQ = new Quaternion().fromAngleAxis(camYaw, Vector3f.UNIT_Y);
Quaternion pitchQ = new Quaternion().fromAngleAxis(camPitch, Vector3f.UNIT_X);
@@ -216,78 +665,93 @@ public class TerrainEditorState extends BaseAppState {
cam.setLocation(camPos);
}
- private void processEdits() {
- SharedInput.TerrainEdit edit;
- while ((edit = input.editQueue.poll()) != null) {
- // JavaFX-Koordinaten → JME3-Screen-Koordinaten (Y spiegeln)
- float jmeX = (float)(edit.screenX() * input.viewportScaleX);
- float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
+ // ── Hilfsobjekte ─────────────────────────────────────────────────────────
- Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
- Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
- Vector3f dir = far.subtract(near).normalizeLocal();
+ private Geometry buildWater() {
+ float half = TERRAIN_SIZE * 0.5f;
+ Geometry water = new Geometry("water", new Quad(TERRAIN_SIZE, TERRAIN_SIZE));
+ water.rotate(-FastMath.HALF_PI, 0, 0);
+ water.setLocalTranslation(-half, 0.01f, half);
- CollisionResults hits = new CollisionResults();
- terrainGeo.collideWith(new com.jme3.math.Ray(near, dir), hits);
-
- if (hits.size() > 0) {
- Vector3f contact = hits.getClosestCollision().getContactPoint();
- modifyHeight(contact, edit.action() * BRUSH_DELTA);
- }
- }
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.05f, 0.25f, 0.70f, 0.55f));
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ water.setQueueBucket(RenderQueue.Bucket.Transparent);
+ water.setMaterial(mat);
+ return water;
}
- // ── Höhen-Werkzeug ───────────────────────────────────────────────────────
+ private Geometry buildGrid() {
+ float step = 8f;
+ float half = TERRAIN_SIZE * 0.5f;
+ int divs = (int)(TERRAIN_SIZE / step);
+ float y = 1.05f;
- private void modifyHeight(Vector3f worldContact, float delta) {
- // Terrain-Geometrie ist bei (-8, 0, -8), Vertices bei (0..16, h, 0..16)
- float localX = worldContact.x + 8;
- float localZ = worldContact.z + 8;
+ int linesPerAxis = divs + 1;
+ int totalVerts = linesPerAxis * 4;
- for (int z = 0; z < V; z++) {
- for (int x = 0; x < V; x++) {
- float dx = x - localX;
- float dz = z - localZ;
- float dist = FastMath.sqrt(dx * dx + dz * dz);
- if (dist < BRUSH_RADIUS) {
- float falloff = 1f - dist / BRUSH_RADIUS;
- heights[z * V + x] += delta * falloff;
- }
- }
+ FloatBuffer pos = BufferUtils.createFloatBuffer(totalVerts * 3);
+ for (int i = 0; i <= divs; i++) {
+ float z = -half + i * step;
+ pos.put(-half).put(y).put(z);
+ pos.put( half).put(y).put(z);
}
- updateTerrainMesh();
+ for (int i = 0; i <= divs; i++) {
+ float x = -half + i * step;
+ pos.put(x).put(y).put(-half);
+ pos.put(x).put(y).put( half);
+ }
+
+ int totalLines = linesPerAxis * 2;
+ IntBuffer idx = BufferUtils.createIntBuffer(totalLines * 2);
+ for (int i = 0; i < totalLines; i++) {
+ idx.put(i * 2);
+ idx.put(i * 2 + 1);
+ }
+
+ Mesh mesh = new Mesh();
+ mesh.setMode(Mesh.Mode.Lines);
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
+ mesh.setBuffer(VertexBuffer.Type.Index, 2, idx);
+ mesh.updateBound();
+
+ Geometry geo = new Geometry("terrainGrid", mesh);
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.5f, 0.5f, 0.5f, 0.4f));
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ geo.setQueueBucket(RenderQueue.Bucket.Transparent);
+ geo.setMaterial(mat);
+ return geo;
}
- private void updateTerrainMesh() {
- FloatBuffer posBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Position);
- FloatBuffer normBuf = terrainMesh.getFloatBuffer(VertexBuffer.Type.Normal);
-
- posBuf.rewind();
- for (int z = 0; z < V; z++) {
- for (int x = 0; x < V; x++) {
- posBuf.put(x).put(heights[z * V + x]).put(z);
- }
+ private Geometry buildBrushIndicator() {
+ int segments = 48;
+ FloatBuffer pos = BufferUtils.createFloatBuffer((segments + 1) * 3);
+ pos.put(0f).put(0f).put(0f);
+ for (int i = 0; i < segments; i++) {
+ float a = FastMath.TWO_PI * i / segments;
+ pos.put(FastMath.cos(a)).put(0f).put(FastMath.sin(a));
}
-
- // Normalen per finite differences
- normBuf.rewind();
- for (int z = 0; z < V; z++) {
- for (int x = 0; x < V; x++) {
- float hL = x > 0 ? heights[z * V + (x - 1)] : heights[z * V + x];
- float hR = x < V-1 ? heights[z * V + (x + 1)] : heights[z * V + x];
- float hD = z > 0 ? heights[(z - 1) * V + x] : heights[z * V + x];
- float hU = z < V-1 ? heights[(z + 1) * V + x] : heights[z * V + x];
- float nx = -(hR - hL);
- float ny = 2.0f;
- float nz = -(hU - hD);
- float len = FastMath.sqrt(nx*nx + ny*ny + nz*nz);
- normBuf.put(nx / len).put(ny / len).put(nz / len);
- }
+ IntBuffer idx = BufferUtils.createIntBuffer(segments * 3);
+ for (int i = 0; i < segments; i++) {
+ idx.put(0);
+ idx.put(1 + i);
+ idx.put(1 + (i + 1) % segments);
}
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
+ mesh.updateBound();
- terrainMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
- terrainMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
- terrainMesh.updateBound();
- terrainGeo.updateModelBound();
+ Geometry geo = new Geometry("brushIndicator", mesh);
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(1f, 0f, 0f, 0.35f));
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ mat.getAdditionalRenderState().setDepthTest(false);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ geo.setQueueBucket(RenderQueue.Bucket.Transparent);
+ geo.setMaterial(mat);
+ geo.setCullHint(Spatial.CullHint.Always);
+ return geo;
}
}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$1.class b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$1.class
new file mode 100644
index 0000000..11dc71f
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$1.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$TreeLodControl.class b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$TreeLodControl.class
new file mode 100644
index 0000000..de9e48b
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState$TreeLodControl.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.class b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.class
new file mode 100644
index 0000000..50b0196
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java
new file mode 100644
index 0000000..afee952
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java
@@ -0,0 +1,671 @@
+package de.blight.editor.state;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.imageio.ImageIO;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.plugins.FileLocator;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.post.SceneProcessor;
+import com.jme3.profile.AppProfiler;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.shadow.DirectionalLightShadowRenderer;
+import com.jme3.shadow.EdgeFilteringMode;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.BufferUtils;
+import com.jme3.util.SkyFactory;
+
+import de.blight.editor.FrameTransfer;
+import de.blight.editor.SharedInput;
+import de.blight.editor.tree.TreeMeshBuilder;
+import de.blight.editor.tree.TreeParams;
+
+/**
+ * JME3-Zustand für den prozeduralen Baum-Generator.
+ *
+ * Ablauf pro Request:
+ * 1. HD-Mesh + LD-Mesh generieren
+ * 2. Impostor-Textur per Offscreen-Capture (SceneProcessor.postFrame) erzeugen
+ * 3. LOD-Node mit TreeLodControl assemblieren
+ * 4. Optional: .j3o-Export via BinaryExporter
+ */
+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 final SharedInput input;
+
+ private SimpleApplication app;
+ private Node rootNode;
+ private AssetManager assets;
+
+ // ── Preview-Viewport ─────────────────────────────────────────────────────
+ private ViewPort previewVP;
+ private FrameBuffer previewFB;
+ private FrameTransfer previewTransfer;
+ private Node previewScene;
+ private Node previewTreeHolder;
+ private DirectionalLight previewSunLight;
+ private final Vector3f previewTarget = new Vector3f(0f, 5f, 0f);
+ private float previewCamDist = 20f;
+ private int currentPreviewW = PREVIEW_SIZE;
+ private int currentPreviewH = PREVIEW_SIZE;
+
+ // ── Offscreen-Capture-Kontext ─────────────────────────────────────────────
+ private SharedInput.TreeGenRequest pendingRequest = null;
+ private Node pendingHdNode = null;
+ private Node pendingLdNode = null;
+ private TreeMeshBuilder.MeshResult pendingHdResult = null;
+ private Material pendingBarkMat = null;
+ private Material pendingLeafMat = null;
+ private ViewPort captureVP = null;
+ private FrameBuffer captureFB = null;
+ private Texture2D captureTex = null;
+ private volatile boolean captureReady = false; // vom SceneProcessor gesetzt
+
+ public TreeGeneratorState(SharedInput input) { this.input = input; }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.rootNode = this.app.getRootNode();
+ this.assets = app.getAssetManager();
+
+ try {
+ assets.registerLocator(ASSET_ROOT.toAbsolutePath().toString(), FileLocator.class);
+ } catch (Exception ignored) {}
+ // Dedizierter Offscreen-Viewport für die Vorschau
+ previewFB = buildFrameBuffer(PREVIEW_SIZE, PREVIEW_SIZE);
+
+ Camera previewCam = new Camera(PREVIEW_SIZE, PREVIEW_SIZE);
+ previewCam.setFrustumPerspective(45f, 1f, 0.1f, 5000f);
+
+ previewVP = this.app.getRenderManager().createPostView("treePreview", previewCam);
+ previewVP.setOutputFrameBuffer(previewFB);
+ previewVP.setBackgroundColor(new ColorRGBA(0.50f, 0.72f, 0.95f, 1f));
+ previewVP.setClearFlags(true, true, true);
+
+ previewScene = new Node("previewScene");
+ previewSunLight = new DirectionalLight(
+ new Vector3f(-0.45f, -1.0f, -0.3f).normalizeLocal(),
+ new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
+ previewScene.addLight(previewSunLight);
+ previewScene.addLight(new DirectionalLight(
+ new Vector3f(0.4f, -0.6f, -0.8f).normalizeLocal(),
+ new ColorRGBA(0.35f, 0.40f, 0.55f, 1f)));
+ previewScene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.30f, 1f)));
+ previewTreeHolder = new Node("treeHolder");
+ previewScene.attachChild(previewTreeHolder);
+ previewScene.attachChild(buildPreviewGround());
+ previewScene.attachChild(buildPreviewSky());
+ previewVP.attachScene(previewScene);
+
+ DirectionalLightShadowRenderer shadowRenderer =
+ new DirectionalLightShadowRenderer(assets, 2048, 1);
+ shadowRenderer.setLight(previewSunLight);
+ shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
+ shadowRenderer.setShadowIntensity(0.55f);
+ shadowRenderer.setShadowZExtend(80f);
+ previewVP.addProcessor(shadowRenderer);
+ previewTransfer = new FrameTransfer(input.treePreviewImage);
+ previewVP.addProcessor(previewTransfer);
+ }
+
+ /**
+ * Wird von EzTreeState aufgerufen, um einen extern generierten Baum
+ * im gemeinsamen Vorschau-Viewport anzuzeigen.
+ */
+ public void setPreviewContent(com.jme3.scene.Node node, float camDist,
+ com.jme3.math.Vector3f target) {
+ previewTreeHolder.detachAllChildren();
+ if (node != null) previewTreeHolder.attachChild(node);
+ this.previewCamDist = camDist;
+ this.previewTarget.set(target);
+ }
+
+ @Override protected void cleanup(Application app) {
+ if (previewVP != null) {
+ this.app.getRenderManager().removePostView(previewVP);
+ previewVP = null;
+ }
+ }
+ @Override protected void onEnable() {}
+ @Override protected void onDisable() {}
+
+ // ── Update-Schleife ───────────────────────────────────────────────────────
+
+ @Override
+ public void update(float tpf) {
+ // 1. Szenen-Änderungen zuerst (bevor updateGeometricState)
+ if (pendingRequest != null && captureReady) {
+ finishCapture();
+ } else if (pendingRequest == null) {
+ SharedInput.TreeGenRequest req = input.treeGenQueue.poll();
+ if (req != null) startGeneration(req);
+ }
+
+ // 2. Framebuffer-Resize falls JavaFX eine neue Größe gemeldet hat
+ int reqW = Math.max(64, input.treePreviewW);
+ int reqH = Math.max(64, input.treePreviewH);
+ if (previewVP != null
+ && (Math.abs(reqW - currentPreviewW) > 8 || Math.abs(reqH - currentPreviewH) > 8)) {
+ resizePreviewViewport(reqW, reqH);
+ }
+
+ // 3. Kamera-Orbit + updateGeometricState immer zuletzt –
+ // nach allen Szenenänderungen dieser Frame
+ if (previewVP != null) {
+ float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD;
+ float rotX = input.treePreviewRotX * FastMath.DEG_TO_RAD;
+ float dist = previewCamDist * input.treePreviewZoom;
+ float cosX = FastMath.cos(rotX);
+ Camera c = previewVP.getCamera();
+ c.setLocation(new Vector3f(
+ FastMath.sin(rotY) * cosX * dist,
+ previewTarget.y + FastMath.sin(rotX) * dist,
+ FastMath.cos(rotY) * cosX * dist));
+ c.lookAt(previewTarget, Vector3f.UNIT_Y);
+ previewScene.updateGeometricState();
+ }
+ }
+
+ // ── Framebuffer-Helpers ───────────────────────────────────────────────────
+
+ private FrameBuffer buildFrameBuffer(int w, int h) {
+ FrameBuffer fb = new FrameBuffer(w, h, 1);
+ fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8));
+ fb.setDepthTexture(new Texture2D(w, h, Image.Format.Depth));
+ return fb;
+ }
+
+ private void resizePreviewViewport(int newW, int newH) {
+ currentPreviewW = newW;
+ currentPreviewH = newH;
+
+ // Alten FrameTransfer entfernen, alten Framebuffer freigeben
+ previewVP.removeProcessor(previewTransfer);
+ try { previewFB.dispose(); } catch (Exception ignored) {}
+
+ // Neuen Framebuffer setzen
+ previewFB = buildFrameBuffer(newW, newH);
+ previewVP.setOutputFrameBuffer(previewFB);
+
+ // Kamera-Aspect anpassen
+ Camera cam = previewVP.getCamera();
+ cam.resize(newW, newH, true);
+ cam.setFrustumPerspective(45f, (float) newW / newH, 0.1f, 5000f);
+
+ // Neue WritableImage + neuen FrameTransfer
+ javafx.scene.image.WritableImage newImg =
+ new javafx.scene.image.WritableImage(newW, newH);
+ input.treePreviewImage = newImg;
+ previewTransfer = new FrameTransfer(newImg);
+ previewVP.addProcessor(previewTransfer);
+
+ // JavaFX signalisieren (nach dem Schreiben von treePreviewImage)
+ input.treePreviewResized = true;
+ }
+
+ // ── Phase 1: Generierung + Capture-Setup ──────────────────────────────────
+
+ @SuppressWarnings("deprecation")
+ private void startGeneration(SharedInput.TreeGenRequest req) {
+ previewTreeHolder.detachAllChildren();
+ cleanupCapture();
+
+ TreeParams p = req.params();
+ TreeMeshBuilder builder = new TreeMeshBuilder();
+
+ TreeMeshBuilder.MeshResult hd = builder.build(p, 1.0f);
+ TreeMeshBuilder.MeshResult ld = builder.build(p, 0.0f);
+
+ Material barkMat = buildBarkMaterial(p);
+ Material leafMat = buildLeafMaterial(p);
+
+ Node hdNode = makeTreeNode(hd, barkMat, leafMat, "hd");
+ Node ldNode = makeTreeNode(ld, barkMat.clone(), leafMat.clone(), "ld");
+
+ // Capture-Viewport aufbauen
+ captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
+ captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
+ captureFB.addColorTexture(captureTex);
+ captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
+
+ captureVP = buildCaptureViewPort(hdNode, hd.bounds(), captureFB);
+ captureReady = false;
+ pendingRequest = req;
+ pendingHdNode = hdNode;
+ pendingLdNode = ldNode;
+ pendingHdResult = hd;
+ pendingBarkMat = barkMat;
+ pendingLeafMat = leafMat;
+
+ input.treeGenStatusMsg = "Rendere Impostor…";
+ }
+
+ // ── Phase 2: Capture abschließen ──────────────────────────────────────────
+
+ private void finishCapture() {
+ // Pixel aus Framebuffer lesen
+ ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
+ app.getRenderer().readFrameBuffer(captureFB, pixels);
+ cleanupCapture();
+
+ Texture2D impostorTex = saveImpostor(pixels, "impostor_" + pendingRequest.exportName());
+
+ // HD-Mesh im Dialog-Preview anzeigen (keine LOD-Umschaltung, kein Welt-Platzierung)
+ Node previewTree = makeTreeNode(pendingHdResult,
+ pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
+ previewTreeHolder.detachAllChildren();
+ previewTreeHolder.attachChild(previewTree);
+
+ BoundingBox bb = pendingHdResult.bounds();
+ previewTarget.set(0f, bb.getCenter().y, 0f);
+ previewCamDist = Math.max(bb.getXExtent(),
+ Math.max(bb.getYExtent(), bb.getZExtent())) * 3f;
+
+ if (pendingRequest.exportAfter()) {
+ Node treeNode = assembleLodNode(impostorTex);
+ exportTree(treeNode, pendingRequest.exportName());
+ } else {
+ input.treeGenStatusMsg = "Vorschau: '" + pendingRequest.exportName() + "'";
+ }
+
+ pendingRequest = null;
+ pendingHdNode = null;
+ pendingLdNode = null;
+ pendingHdResult = null;
+ pendingBarkMat = null;
+ pendingLeafMat = null;
+ }
+
+ // ── LOD-Aufbau ────────────────────────────────────────────────────────────
+
+ private Node assembleLodNode(Texture2D impostorTex) {
+ Node root = new Node("GeneratedTree_" + pendingRequest.exportName());
+ root.attachChild(pendingHdNode);
+ root.attachChild(pendingLdNode);
+
+ Node lod2 = makeImpostorNode(pendingHdResult.bounds(), impostorTex);
+ root.attachChild(lod2);
+
+ // Nur LOD0 initial sichtbar; Control steuert je nach Distanz
+ pendingLdNode.setCullHint(Spatial.CullHint.Always);
+ lod2.setCullHint(Spatial.CullHint.Always);
+
+ root.addControl(new TreeLodControl(app.getCamera(),
+ pendingHdNode, pendingLdNode, lod2, 60f, 200f));
+ return root;
+ }
+
+ private Node makeImpostorNode(BoundingBox bb, Texture2D tex) {
+ float h = bb.getYExtent() * 2f;
+ float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f;
+ float size = Math.max(h, w);
+ float yOff = bb.getCenter().y + 2f; // passt zur Baum-Offset-Y
+
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ if (tex != null) mat.setTexture("ColorMap", tex);
+ else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+
+ Node n = new Node("lod2");
+ n.attachChild(buildBillboardQuad("quad_a", 0f, yOff, size, mat));
+ n.attachChild(buildBillboardQuad("quad_b", FastMath.HALF_PI, yOff, size, mat.clone()));
+ n.setQueueBucket(RenderQueue.Bucket.Transparent);
+ return n;
+ }
+
+ private Geometry buildBillboardQuad(String name, float yRot, float yCent,
+ float size, Material mat) {
+ float hw = size * 0.5f;
+ float hh = size * 0.5f;
+ float cos = FastMath.cos(yRot);
+ float sin = FastMath.sin(yRot);
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{
+ -hw*cos, yCent-hh, -hw*sin,
+ hw*cos, yCent-hh, hw*sin,
+ hw*cos, yCent+hh, hw*sin,
+ -hw*cos, yCent+hh, -hw*sin
+ });
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ 0,0, 1,0, 1,1, 0,1 });
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
+ mesh.updateBound();
+
+ Geometry g = new Geometry(name, mesh);
+ g.setMaterial(mat);
+ return g;
+ }
+
+ // ── Material-Factories ────────────────────────────────────────────────────
+
+ private Material buildBarkMaterial(TreeParams p) {
+ try {
+ Material mat = new Material(assets, "MatDefs/Tree.j3md");
+ mat.setColor("Diffuse", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f));
+ mat.setFloat("WindStrength", 0.15f);
+ mat.setFloat("WindSpeed", 0.5f);
+ if (p.barkTexture != null) {
+ try {
+ mat.setTexture("BarkMap", assets.loadTexture(p.barkTexture));
+ mat.setBoolean("HasBarkMap", true);
+ } catch (Exception tex) {
+ System.err.println("[TreeGenerator] Bark-Textur nicht gefunden: " + p.barkTexture);
+ }
+ }
+ return mat;
+ } catch (Exception e) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f));
+ return mat;
+ }
+ }
+
+ private Material buildLeafMaterial(TreeParams p) {
+ try {
+ Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
+ mat.setColor("Diffuse", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f));
+ mat.setFloat("WindStrength", 0.30f);
+ mat.setFloat("WindSpeed", 0.7f);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ if (p.leafTexture != null) {
+ try {
+ mat.setTexture("LeafMap", assets.loadTexture(p.leafTexture));
+ mat.setBoolean("HasLeafMap", true);
+ } catch (Exception tex) {
+ System.err.println("[TreeGenerator] Blatt-Textur nicht gefunden: " + p.leafTexture);
+ }
+ }
+ return mat;
+ } catch (Exception e) {
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f));
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ }
+ }
+
+ // ── Tree-Node aus MeshResult ──────────────────────────────────────────────
+
+ private Node makeTreeNode(TreeMeshBuilder.MeshResult r,
+ Material barkMat, Material leafMat, String tag) {
+ Node n = new Node("tree_" + tag);
+ if (r.bark().getVertexCount() > 0) {
+ Geometry g = new Geometry("bark_" + tag, r.bark());
+ g.setMaterial(barkMat);
+ g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ n.attachChild(g);
+ }
+ if (r.leaves().getVertexCount() > 0) {
+ Geometry g = new Geometry("leaves_" + tag, r.leaves());
+ g.setMaterial(leafMat);
+ g.setQueueBucket(RenderQueue.Bucket.Transparent);
+ g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ n.attachChild(g);
+ }
+ return n;
+ }
+
+ // ── Offscreen-ViewPort ────────────────────────────────────────────────────
+
+ private ViewPort buildCaptureViewPort(Node treeNode, BoundingBox bb, FrameBuffer fb) {
+ Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
+ Vector3f center = bb.getCenter().add(0f, 2f, 0f);
+ float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
+ float dist = extent * 3.0f;
+
+ cam.setLocation(center.add(0f, 0f, dist));
+ cam.lookAt(center, Vector3f.UNIT_Y);
+ cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
+
+ ViewPort vp = app.getRenderManager()
+ .createPostView("impostorCap_" + System.nanoTime(), cam);
+ vp.setOutputFrameBuffer(fb);
+ vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
+ vp.setClearFlags(true, true, true);
+
+ // Capture-Szene: Kopien der Geometrien + Beleuchtung
+ Node scene = new Node("captureScene");
+ scene.addLight(new DirectionalLight(
+ new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
+ scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
+
+ Node capTree = cloneForCapture(treeNode);
+ scene.attachChild(capTree);
+ vp.attachScene(scene);
+ scene.updateGeometricState();
+
+ // Einmaliger SceneProcessor signalisiert Fertigstellung
+ vp.addProcessor(new SceneProcessor() {
+ @Override public void initialize(RenderManager rm, ViewPort v) {}
+ @Override public void reshape(ViewPort v, int w, int h) {}
+ @Override public boolean isInitialized() { return true; }
+ @Override public void preFrame(float t) {}
+ @Override public void postQueue(RenderQueue rq) {}
+ @Override public void cleanup() {}
+ @Override public void setProfiler(AppProfiler profiler) {}
+ @Override
+ public void postFrame(FrameBuffer out) {
+ vp.removeProcessor(this);
+ captureReady = true;
+ }
+ });
+
+ return vp;
+ }
+
+ private Node cloneForCapture(Node src) {
+ Node copy = new Node(src.getName() + "_cap");
+ copy.setLocalTranslation(src.getLocalTranslation());
+ for (Spatial child : src.getChildren()) {
+ if (child instanceof Geometry g) {
+ Geometry gc = new Geometry(g.getName() + "_c", g.getMesh());
+ gc.setMaterial(g.getMaterial().clone());
+ copy.attachChild(gc);
+ }
+ }
+ return copy;
+ }
+
+ // ── Impostor-PNG speichern ────────────────────────────────────────────────
+
+ private Texture2D saveImpostor(ByteBuffer pixels, String name) {
+ try {
+ pixels.rewind();
+ BufferedImage img = new BufferedImage(
+ IMPOSTOR_SIZE, IMPOSTOR_SIZE, BufferedImage.TYPE_INT_ARGB);
+ for (int y = 0; y < IMPOSTOR_SIZE; y++) {
+ for (int x = 0; x < IMPOSTOR_SIZE; x++) {
+ int r = pixels.get() & 0xFF;
+ int g = pixels.get() & 0xFF;
+ int b = pixels.get() & 0xFF;
+ int a = pixels.get() & 0xFF;
+ img.setRGB(x, IMPOSTOR_SIZE - 1 - y, (a<<24)|(r<<16)|(g<<8)|b);
+ }
+ }
+ Path texDir = ASSET_ROOT.resolve("textures");
+ Files.createDirectories(texDir);
+ File pngFile = texDir.resolve(name + ".png").toFile();
+ ImageIO.write(img, "PNG", pngFile);
+ System.out.println("[TreeGenerator] Impostor: " + pngFile.getAbsolutePath());
+
+ try {
+ return (Texture2D) assets.loadTexture("textures/" + name + ".png");
+ } catch (Exception loadEx) {
+ pixels.rewind();
+ Image jmeImg = new Image(Image.Format.RGBA8, IMPOSTOR_SIZE, IMPOSTOR_SIZE,
+ pixels, null, com.jme3.texture.image.ColorSpace.sRGB);
+ return new Texture2D(jmeImg);
+ }
+ } catch (IOException e) {
+ System.err.println("[TreeGenerator] Impostor-Fehler: " + e.getMessage());
+ return null;
+ }
+ }
+
+ // ── .j3o-Export ───────────────────────────────────────────────────────────
+
+ private void exportTree(Node treeNode, String name) {
+ try {
+ Path modelDir = ASSET_ROOT.resolve("models");
+ Files.createDirectories(modelDir);
+ File out = modelDir.resolve("GeneratedTree_" + name + ".j3o").toFile();
+ // Strip runtime controls before export — they lack no-arg constructors
+ // and cannot be deserialized by BinaryImporter.
+ while (treeNode.getNumControls() > 0)
+ treeNode.removeControl(treeNode.getControl(0));
+ BinaryExporter.getInstance().save(treeNode, out);
+ input.treeGenStatusMsg = "Exportiert: " + out.getName();
+ input.refreshAssets = true;
+ System.out.println("[TreeGenerator] Exportiert: " + out.getAbsolutePath());
+ } catch (IOException e) {
+ input.treeGenStatusMsg = "Export-Fehler: " + e.getMessage();
+ System.err.println("[TreeGenerator] Export-Fehler: " + e.getMessage());
+ }
+ }
+
+ // ── Aufräumen ─────────────────────────────────────────────────────────────
+
+ private void cleanupCapture() {
+ if (captureVP != null) {
+ app.getRenderManager().removePostView(captureVP);
+ captureVP = null;
+ }
+ if (captureFB != null) {
+ try { captureFB.dispose(); } catch (Exception ignored) {}
+ captureFB = null;
+ }
+ captureTex = null;
+ captureReady = false;
+ }
+
+ // ── LOD-Control ───────────────────────────────────────────────────────────
+
+ private static final class TreeLodControl extends AbstractControl {
+ private final Camera cam;
+ private final Node lod0, lod1, lod2;
+ private final float d01sq, d12sq;
+
+ TreeLodControl(Camera cam, Node l0, Node l1, Node l2, float d01, float d12) {
+ this.cam = cam;
+ this.lod0 = l0; this.lod1 = l1; this.lod2 = l2;
+ this.d01sq = d01 * d01;
+ this.d12sq = d12 * d12;
+ }
+
+ @Override
+ protected void controlUpdate(float tpf) {
+ float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
+ lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
+ lod1.setCullHint(dSq>=d01sq && dSq= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
+ }
+
+ @Override protected void controlRender(RenderManager rm, ViewPort vp) {}
+ }
+
+ // ── Vorschau-Boden (groß, Gras-Textur) ──────────────────────────────────
+
+ private Geometry buildPreviewGround() {
+ float size = 600f;
+ float tiles = 30f; // UV-Wiederholungen
+
+ // Eigenes Mesh mit gekachelten UVs (Quad unterstützt kein Tiling)
+ com.jme3.scene.Mesh mesh = new com.jme3.scene.Mesh();
+ float h = size * 0.5f;
+ mesh.setBuffer(VertexBuffer.Type.Position, 3,
+ new float[]{ -h,0,-h, h,0,-h, h,0,h, -h,0,h });
+ mesh.setBuffer(VertexBuffer.Type.Normal, 3,
+ new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 });
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
+ new float[]{ 0,0, tiles,0, tiles,tiles, 0,tiles });
+ mesh.setBuffer(VertexBuffer.Type.Index, 3,
+ new int[]{ 0,2,1, 0,3,2 });
+ mesh.updateBound();
+
+ Geometry ground = new Geometry("previewGround", mesh);
+
+ Material mat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
+ mat.setBoolean("UseMaterialColors", true);
+ mat.setColor("Diffuse", new ColorRGBA(0.28f, 0.45f, 0.14f, 1f));
+ mat.setColor("Ambient", new ColorRGBA(0.11f, 0.18f, 0.06f, 1f));
+ mat.setColor("Specular", ColorRGBA.Black);
+ mat.setFloat("Shininess", 0f);
+
+ try {
+ Texture grassTex = assets.loadTexture("Textures/gras.png");
+ grassTex.setWrap(Texture.WrapMode.Repeat);
+ mat.setTexture("DiffuseMap", grassTex);
+ } catch (Exception ignored) {
+ // Fallback auf Farbe, wenn Textur fehlt
+ }
+
+ ground.setMaterial(mat);
+ ground.setShadowMode(RenderQueue.ShadowMode.Receive);
+ return ground;
+ }
+
+ // ── Skybox (Kuppel) ───────────────────────────────────────────────────────
+
+ private Spatial buildPreviewSky() {
+ // Versuche zuerst SkyFactory mit einer Sphere-Map-Textur
+ String[] skyPaths = { "Textures/sky.png", "Textures/Sky.png", "Textures/skybox.png" };
+ for (String path : skyPaths) {
+ try {
+ Texture skyTex = assets.loadTexture(path);
+ return SkyFactory.createSky(assets, skyTex, SkyFactory.EnvMapType.SphereMap);
+ } catch (Exception ignored) {}
+ }
+
+ // Fallback: gefärbte Innenkugel als einfache Himmelskuppel
+ Sphere dome = new Sphere(16, 32, 800f, false, true);
+ Geometry sky = new Geometry("previewSky", dome);
+ sky.setQueueBucket(RenderQueue.Bucket.Sky);
+ sky.setCullHint(Spatial.CullHint.Never);
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.50f, 0.72f, 0.95f, 1f));
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ sky.setMaterial(mat);
+ return sky;
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.class b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.class
new file mode 100644
index 0000000..ff77789
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.java
new file mode 100644
index 0000000..7d3feba
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerData.java
@@ -0,0 +1,74 @@
+package de.blight.editor.state;
+
+import java.util.Arrays;
+
+/**
+ * Data arrays for the upper (mountain) layer.
+ *
+ * Grid: 512×512 cells → 513×513 vertices, covering 4096×4096 world units
+ * (8 world units per cell). World origin is at grid centre: vertex (256,256)
+ * maps to world (0,0).
+ */
+public class UpperLayerData {
+
+ public static final int CELLS = 512; // cells per axis
+ public static final int VERTS = 513; // vertices per axis (CELLS + 1)
+
+ /** Y of the top surface at each vertex [VERTS*VERTS]. */
+ public final float[] topHeight;
+
+ /** Y of the cave ceiling at each vertex [VERTS*VERTS]. */
+ public final float[] bottomHeight;
+
+ /** 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;
+ /** Dicke der Gesteinsschicht in Welteinheiten. */
+ public static final float LAYER_THICKNESS = 30f;
+
+ public UpperLayerData() {
+ topHeight = new float[VERTS * VERTS];
+ bottomHeight = new float[VERTS * VERTS];
+ hole = new boolean[CELLS * CELLS];
+
+ Arrays.fill(topHeight, INITIAL_TERRAIN_Y);
+ Arrays.fill(bottomHeight, INITIAL_TERRAIN_Y - LAYER_THICKNESS);
+ // hole[] defaults to false
+ }
+
+ // ── Convenience accessors ────────────────────────────────────────────────
+
+ /** Returns true when the cell is outside the grid (treated as solid). */
+ public boolean isHole(int cx, int cz) {
+ if (cx < 0 || cx >= CELLS || cz < 0 || cz >= CELLS) return false;
+ return hole[cx + cz * CELLS];
+ }
+
+ public float topAt(int vx, int vz) {
+ return topHeight[vx + vz * VERTS];
+ }
+
+ public float bottomAt(int vx, int vz) {
+ return bottomHeight[vx + vz * VERTS];
+ }
+
+ // ── World ↔ grid conversion ──────────────────────────────────────────────
+
+ /** World X/Z → nearest vertex index (clamped). */
+ public static int worldToVertexX(float wx) {
+ return Math.max(0, Math.min(VERTS - 1, Math.round((wx + 2048f) / 8f)));
+ }
+ public static int worldToVertexZ(float wz) {
+ return Math.max(0, Math.min(VERTS - 1, Math.round((wz + 2048f) / 8f)));
+ }
+
+ /** World X/Z → cell index (floored, clamped). */
+ public static int worldToCellX(float wx) {
+ return Math.max(0, Math.min(CELLS - 1, (int) ((wx + 2048f) / 8f)));
+ }
+ public static int worldToCellZ(float wz) {
+ return Math.max(0, Math.min(CELLS - 1, (int) ((wz + 2048f) / 8f)));
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.class b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.class
new file mode 100644
index 0000000..7a30e1d
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.java
new file mode 100644
index 0000000..94be3a4
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerMesher.java
@@ -0,0 +1,194 @@
+package de.blight.editor.state;
+
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.util.BufferUtils;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+
+/**
+ * Builds a single triangle-list {@link Mesh} for one 16×16-cell chunk of the
+ * upper layer. UV coordinates tile once per 8-world-unit cell (= once per
+ * grid cell), matching the terrain grid. Vertical faces use the same scale
+ * so the texture does not stretch on tall walls.
+ *
+ * Winding convention (right-hand, JME3 default back-face cull):
+ * Top face (+Y normal) : FaceA – indices (0,2,3),(0,3,1)
+ * Bot face (−Y normal) : FaceB – indices (0,1,3),(0,3,2)
+ * South wall (+Z normal): FaceA
+ * North wall (−Z normal): FaceB
+ * East wall (+X normal): FaceB
+ * West wall (−X normal): FaceA
+ */
+public final class UpperLayerMesher {
+
+ private static final int CPP = 16; // cells per chunk axis
+ private static final float CS = 8f; // cell size in world units
+ private static final float OFS = -2048f; // world origin offset
+
+ private UpperLayerMesher() {}
+
+ /**
+ * Builds a mesh for chunk (chunkX, chunkZ) where both indices are in
+ * [0, 31]. Returns {@code null} when every cell in the chunk is a hole.
+ */
+ public static Mesh buildChunk(UpperLayerData d, int chunkX, int chunkZ) {
+ ArrayList pos = new ArrayList<>(CPP * CPP * 24 * 3);
+ ArrayList nor = new ArrayList<>(CPP * CPP * 24 * 3);
+ ArrayList uv = new ArrayList<>(CPP * CPP * 24 * 2);
+ ArrayList idx = new ArrayList<>(CPP * CPP * 24);
+
+ int baseCX = chunkX * CPP;
+ int baseCZ = chunkZ * CPP;
+
+ for (int lz = 0; lz < CPP; lz++) {
+ for (int lx = 0; lx < CPP; lx++) {
+ int gcx = baseCX + lx;
+ int gcz = baseCZ + lz;
+
+ if (d.isHole(gcx, gcz)) continue;
+
+ float x0 = gcx * CS + OFS;
+ float x1 = (gcx + 1) * CS + OFS;
+ float z0 = gcz * CS + OFS;
+ float z1 = (gcz + 1) * CS + OFS;
+
+ float t00 = d.topAt(gcx, gcz);
+ float t10 = d.topAt(gcx + 1, gcz);
+ float t01 = d.topAt(gcx, gcz + 1);
+ float t11 = d.topAt(gcx + 1, gcz + 1);
+ float b00 = d.bottomAt(gcx, gcz);
+ float b10 = d.bottomAt(gcx + 1, gcz);
+ float b01 = d.bottomAt(gcx, gcz + 1);
+ float b11 = d.bottomAt(gcx + 1, gcz + 1);
+
+ // Horizontal UV: integer cell indices → 1 tile per cell with Repeat
+ float ux0 = gcx, ux1 = gcx + 1f;
+ float uz0 = gcz, uz1 = gcz + 1f;
+
+ // ── Top face (+Y) ──────────────────────────────────────────────
+ faceA(pos, nor, uv, idx,
+ x0, t00, z0, x1, t10, z0, x0, t01, z1, x1, t11, z1,
+ 0, 1, 0,
+ ux0, uz0, ux1, uz0, ux0, uz1, ux1, uz1);
+
+ // ── Bottom face (−Y) ───────────────────────────────────────────
+ faceB(pos, nor, uv, idx,
+ x0, b00, z0, x1, b10, z0, x0, b01, z1, x1, b11, z1,
+ 0, -1, 0,
+ ux0, uz0, ux1, uz0, ux0, uz1, ux1, uz1);
+
+ // ── North wall (−Z): edge at z0 ───────────────────────────────
+ if (d.isHole(gcx, gcz - 1)) {
+ faceB(pos, nor, uv, idx,
+ x0, t00, z0, x1, t10, z0, x0, b00, z0, x1, b10, z0,
+ 0, 0, -1,
+ ux0, t00/CS, ux1, t10/CS, ux0, b00/CS, ux1, b10/CS);
+ }
+
+ // ── South wall (+Z): edge at z1 ───────────────────────────────
+ if (d.isHole(gcx, gcz + 1)) {
+ faceA(pos, nor, uv, idx,
+ x0, t01, z1, x1, t11, z1, x0, b01, z1, x1, b11, z1,
+ 0, 0, 1,
+ ux0, t01/CS, ux1, t11/CS, ux0, b01/CS, ux1, b11/CS);
+ }
+
+ // ── West wall (−X): edge at x0 ────────────────────────────────
+ if (d.isHole(gcx - 1, gcz)) {
+ faceA(pos, nor, uv, idx,
+ x0, t00, z0, x0, t01, z1, x0, b00, z0, x0, b01, z1,
+ -1, 0, 0,
+ uz0, t00/CS, uz1, t01/CS, uz0, b00/CS, uz1, b01/CS);
+ }
+
+ // ── East wall (+X): edge at x1 ────────────────────────────────
+ if (d.isHole(gcx + 1, gcz)) {
+ faceB(pos, nor, uv, idx,
+ x1, t10, z0, x1, t11, z1, x1, b10, z0, x1, b11, z1,
+ 1, 0, 0,
+ uz0, t10/CS, uz1, t11/CS, uz0, b10/CS, uz1, b11/CS);
+ }
+ }
+ }
+
+ if (idx.isEmpty()) return null;
+ return toMesh(pos, nor, uv, idx);
+ }
+
+ // ── Winding helpers ──────────────────────────────────────────────────────
+
+ private static void faceA(ArrayList pos, ArrayList nor, ArrayList uv,
+ ArrayList idx,
+ 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 tu0, float tv0, float tu1, float tv1,
+ float tu2, float tv2, float tu3, float tv3) {
+ int b = pos.size() / 3;
+ addV(pos, nor, uv, x0, y0, z0, nx, ny, nz, tu0, tv0);
+ addV(pos, nor, uv, x1, y1, z1, nx, ny, nz, tu1, tv1);
+ addV(pos, nor, uv, x2, y2, z2, nx, ny, nz, tu2, tv2);
+ addV(pos, nor, uv, x3, y3, z3, nx, ny, nz, tu3, tv3);
+ idx.add(b); idx.add(b + 2); idx.add(b + 3);
+ idx.add(b); idx.add(b + 3); idx.add(b + 1);
+ }
+
+ private static void faceB(ArrayList pos, ArrayList nor, ArrayList uv,
+ ArrayList idx,
+ 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 tu0, float tv0, float tu1, float tv1,
+ float tu2, float tv2, float tu3, float tv3) {
+ int b = pos.size() / 3;
+ addV(pos, nor, uv, x0, y0, z0, nx, ny, nz, tu0, tv0);
+ addV(pos, nor, uv, x1, y1, z1, nx, ny, nz, tu1, tv1);
+ addV(pos, nor, uv, x2, y2, z2, nx, ny, nz, tu2, tv2);
+ addV(pos, nor, uv, x3, y3, z3, nx, ny, nz, tu3, tv3);
+ idx.add(b); idx.add(b + 1); idx.add(b + 3);
+ idx.add(b); idx.add(b + 3); idx.add(b + 2);
+ }
+
+ private static void addV(ArrayList pos, ArrayList nor, ArrayList uv,
+ float x, float y, float z,
+ float nx, float ny, float nz,
+ float u, float v) {
+ pos.add(x); pos.add(y); pos.add(z);
+ nor.add(nx); nor.add(ny); nor.add(nz);
+ uv.add(u); uv.add(v);
+ }
+
+ // ── List → JME3 Mesh ─────────────────────────────────────────────────────
+
+ private static Mesh toMesh(ArrayList pos,
+ ArrayList nor,
+ ArrayList uv,
+ ArrayList idx) {
+ FloatBuffer pb = BufferUtils.createFloatBuffer(pos.size());
+ for (float v : pos) pb.put(v);
+
+ FloatBuffer nb = BufferUtils.createFloatBuffer(nor.size());
+ for (float v : nor) nb.put(v);
+
+ FloatBuffer ub = BufferUtils.createFloatBuffer(uv.size());
+ for (float v : uv) ub.put(v);
+
+ IntBuffer ib = BufferUtils.createIntBuffer(idx.size());
+ for (int v : idx) ib.put(v);
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, pb);
+ mesh.setBuffer(VertexBuffer.Type.Normal, 3, nb);
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, ub);
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, ib);
+ mesh.updateBound();
+ return mesh;
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.class b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.class
new file mode 100644
index 0000000..f2c08a3
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java
new file mode 100644
index 0000000..ce3fc69
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/state/UpperLayerState.java
@@ -0,0 +1,323 @@
+package de.blight.editor.state;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.math.Vector2f;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.texture.Texture;
+import de.blight.common.MapData;
+import de.blight.editor.SharedInput;
+import de.blight.editor.tool.HoleTool;
+import de.blight.editor.tool.UpperHeightTool;
+
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * AppState that owns the upper (mountain) layer: 1024 chunk geometries
+ * arranged in a 32×32 grid, rebuilt lazily when marked dirty.
+ */
+public class UpperLayerState extends BaseAppState {
+
+ // ── Constants ────────────────────────────────────────────────────────────
+
+ private static final int CHUNKS_PER_AXIS = 32; // 512 cells / 16
+ private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS; // 1024
+ private static final int MAX_REBUILDS_PER_FRAME = 4;
+
+ // ── State ────────────────────────────────────────────────────────────────
+
+ private final SharedInput input;
+ private final MapData initialMapData; // null = Standardwerte
+
+ final UpperLayerData data = new UpperLayerData();
+
+ private Node upperNode;
+ private Material chunkMat;
+ private Geometry[] chunkGeos = new Geometry[CHUNK_COUNT];
+ private boolean[] dirtyChunks = new boolean[CHUNK_COUNT];
+
+ // ── Konstruktoren ─────────────────────────────────────────────────────────
+
+ public UpperLayerState(SharedInput input) {
+ this(input, null);
+ }
+
+ public UpperLayerState(SharedInput input, MapData initialMapData) {
+ this.input = input;
+ this.initialMapData = initialMapData;
+ }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
+
+ @Override
+ protected void initialize(Application app) {
+ AssetManager assets = app.getAssetManager();
+ Node rootNode = ((SimpleApplication) app).getRootNode();
+
+ chunkMat = new Material(assets, "Common/MatDefs/Light/Lighting.j3md");
+ try {
+ Texture rock = assets.loadTexture("Textures/Terrain/Rock2/rock.jpg");
+ rock.setWrap(Texture.WrapMode.Repeat);
+ chunkMat.setTexture("DiffuseMap", rock);
+ chunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.32f, 0.25f, 1f));
+ System.out.println("[UpperLayer] Vulkangestein-Textur geladen");
+ } catch (Exception e) {
+ System.out.println("[UpperLayer] Rock-Textur fehlt, Fallback: " + e.getMessage());
+ chunkMat.setBoolean("UseMaterialColors", true);
+ chunkMat.setColor("Diffuse", new ColorRGBA(0.18f, 0.12f, 0.08f, 1f));
+ }
+ chunkMat.setColor("Ambient", new ColorRGBA(0.10f, 0.07f, 0.05f, 1f));
+ chunkMat.setColor("Specular", new ColorRGBA(0.06f, 0.05f, 0.04f, 1f));
+ chunkMat.setFloat("Shininess", 6f);
+ // Polygon-Offset verhindert Z-Fighting mit dem Gras-Terrain an gleicher Y-Position
+ chunkMat.getAdditionalRenderState().setPolyOffset(2f, 2f);
+
+ // Gespeicherte Karte übernehmen (falls vorhanden)
+ if (initialMapData != null) {
+ System.arraycopy(initialMapData.upperTop, 0, data.topHeight, 0,
+ Math.min(initialMapData.upperTop.length, data.topHeight.length));
+ System.arraycopy(initialMapData.upperBottom, 0, data.bottomHeight, 0,
+ Math.min(initialMapData.upperBottom.length, data.bottomHeight.length));
+ for (int i = 0; i < Math.min(initialMapData.upperHole.length, data.hole.length); i++) {
+ data.hole[i] = initialMapData.upperHole[i] != 0;
+ }
+ }
+
+ upperNode = new Node("upperLayer");
+ rootNode.attachChild(upperNode);
+
+ // Alle Chunks beim Start aufbauen
+ for (int i = 0; i < CHUNK_COUNT; i++) {
+ rebuildChunk(i);
+ }
+ }
+
+ @Override
+ protected void onEnable() { upperNode.setCullHint(com.jme3.scene.Spatial.CullHint.Inherit); }
+
+ @Override
+ protected void onDisable() { upperNode.setCullHint(com.jme3.scene.Spatial.CullHint.Always); }
+
+ @Override
+ protected void cleanup(Application app) {
+ ((SimpleApplication) app).getRootNode().detachChild(upperNode);
+ }
+
+ // ── Update loop ───────────────────────────────────────────────────────────
+
+ @Override
+ public void update(float tpf) {
+ // Show/hide based on SharedInput flag
+ boolean shouldShow = input.upperLayerVisible;
+ com.jme3.scene.Spatial.CullHint hint = shouldShow
+ ? com.jme3.scene.Spatial.CullHint.Inherit
+ : com.jme3.scene.Spatial.CullHint.Always;
+ if (upperNode.getCullHint() != hint) {
+ upperNode.setCullHint(hint);
+ }
+
+ int rebuilt = 0;
+ for (int i = 0; i < CHUNK_COUNT && rebuilt < MAX_REBUILDS_PER_FRAME; i++) {
+ if (dirtyChunks[i]) {
+ rebuildChunk(i);
+ dirtyChunks[i] = false;
+ rebuilt++;
+ }
+ }
+ }
+
+ // ── Public API ────────────────────────────────────────────────────────────
+
+ /** The Node containing all chunk geometries — use for ray-casting. */
+ public Node getUpperNode() { return upperNode; }
+
+ public void setTopHeight(int vx, int vz, float h) {
+ data.topHeight[vx + vz * UpperLayerData.VERTS] = h;
+ markVertexDirty(vx, vz);
+ }
+
+ public void setHole(int cx, int cz, boolean v) {
+ if (cx < 0 || cx >= UpperLayerData.CELLS || cz < 0 || cz >= UpperLayerData.CELLS) return;
+ data.hole[cx + cz * UpperLayerData.CELLS] = v;
+ // Cell itself + all 4 neighbours (their wall geometry may change)
+ markCellDirty(cx, cz);
+ markCellDirty(cx - 1, cz);
+ markCellDirty(cx + 1, cz);
+ markCellDirty(cx, cz - 1);
+ markCellDirty(cx, cz + 1);
+ }
+
+ // ── Height editing ────────────────────────────────────────────────────────
+
+ public void applyHeightEdit(float worldX, float worldZ, int action) {
+ int mode = input.upperHeightTool.mode.getSelectedIndex();
+ float radius = (float) input.upperHeightTool.brushRadius.getValue();
+ float str = (float) input.upperHeightTool.brushStrength.getValue();
+
+ if (mode == UpperHeightTool.MODE_SMOOTH) {
+ smoothHeight(worldX, worldZ, radius, str);
+ return;
+ }
+
+ int cx = UpperLayerData.worldToVertexX(worldX);
+ int cz = UpperLayerData.worldToVertexZ(worldZ);
+ int r = (int) Math.ceil(radius / 8f); // radius in vertex units
+
+ float sign = (mode == UpperHeightTool.MODE_LOWER) ? -1f : 1f;
+ float delta = sign * str * action;
+
+ // Flatten target: height at brush centre
+ float flatTarget = data.topAt(cx, cz);
+
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ int vx = cx + dx, vz = cz + dz;
+ if (vx < 0 || vx >= UpperLayerData.VERTS) continue;
+ if (vz < 0 || vz >= UpperLayerData.VERTS) continue;
+
+ float dist = FastMath.sqrt(dx * dx + dz * dz);
+ if (dist >= r + 1) continue;
+
+ float t = dist / (r + 1);
+ float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
+
+ float cur = data.topAt(vx, vz);
+ float next;
+ if (mode == UpperHeightTool.MODE_FLATTEN) {
+ next = cur + (flatTarget - cur) * falloff * str * 3f;
+ } else {
+ next = cur + delta * falloff;
+ }
+ setTopHeight(vx, vz, next);
+ }
+ }
+ }
+
+ private void smoothHeight(float worldX, float worldZ, float radius, float str) {
+ int cx = UpperLayerData.worldToVertexX(worldX);
+ int cz = UpperLayerData.worldToVertexZ(worldZ);
+ int r = (int) Math.ceil(radius / 8f);
+
+ // Average height in brush
+ float sum = 0f; int count = 0;
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ int vx = cx + dx, vz = cz + dz;
+ if (vx < 0 || vx >= UpperLayerData.VERTS) continue;
+ if (vz < 0 || vz >= UpperLayerData.VERTS) continue;
+ if (FastMath.sqrt(dx * dx + dz * dz) >= r + 1) continue;
+ sum += data.topAt(vx, vz);
+ count++;
+ }
+ }
+ if (count == 0) return;
+ float avg = sum / count;
+
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ int vx = cx + dx, vz = cz + dz;
+ if (vx < 0 || vx >= UpperLayerData.VERTS) continue;
+ if (vz < 0 || vz >= UpperLayerData.VERTS) continue;
+ float dist = FastMath.sqrt(dx * dx + dz * dz);
+ if (dist >= r + 1) continue;
+ float falloff = 1f - dist / (r + 1);
+ float cur = data.topAt(vx, vz);
+ setTopHeight(vx, vz, cur + (avg - cur) * falloff * str * 3f);
+ }
+ }
+ }
+
+ // ── Hole editing ──────────────────────────────────────────────────────────
+
+ public void applyHoleEdit(float worldX, float worldZ) {
+ boolean dig = input.holeTool.mode.getSelectedIndex() == HoleTool.MODE_DIG;
+ float radius = (float) input.holeTool.brushRadius.getValue();
+ int cx = UpperLayerData.worldToCellX(worldX);
+ int cz = UpperLayerData.worldToCellZ(worldZ);
+ int r = (int) Math.ceil(radius / 8f);
+
+ for (int dz = -r; dz <= r; dz++) {
+ for (int dx = -r; dx <= r; dx++) {
+ if (FastMath.sqrt(dx * dx + dz * dz) > r) continue;
+ setHole(cx + dx, cz + dz, dig);
+ }
+ }
+ }
+
+ // ── Terrain-Sync ──────────────────────────────────────────────────────────
+
+ /**
+ * 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.
+ */
+ public void adjustHeightsWithTerrain(List worldXZ, List deltas) {
+ HashSet seen = new HashSet<>();
+ 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 key = uvx + uvz * UpperLayerData.VERTS;
+ if (!seen.add(key)) continue;
+ float d = deltas.get(i);
+ data.topHeight[key] += d;
+ data.bottomHeight[key] += d;
+ markVertexDirty(uvx, uvz);
+ }
+ }
+
+ // ── Dirty tracking ────────────────────────────────────────────────────────
+
+ /** Mark all chunks that contain vertex (vx, vz). A vertex is shared by up to 4 cells/chunks. */
+ private void markVertexDirty(int vx, int vz) {
+ // The vertex sits on the corner of cells (vx-1,vz-1)..(vx,vz)
+ for (int dcz = -1; dcz <= 0; dcz++) {
+ for (int dcx = -1; dcx <= 0; dcx++) {
+ int cx = vx + dcx;
+ int cz = vz + dcz;
+ if (cx < 0 || cx >= UpperLayerData.CELLS) continue;
+ if (cz < 0 || cz >= UpperLayerData.CELLS) continue;
+ markCellDirty(cx, cz);
+ }
+ }
+ }
+
+ private void markCellDirty(int cx, int cz) {
+ if (cx < 0 || cx >= UpperLayerData.CELLS) return;
+ if (cz < 0 || cz >= UpperLayerData.CELLS) return;
+ int chunkX = cx / 16;
+ int chunkZ = cz / 16;
+ dirtyChunks[chunkX + chunkZ * CHUNKS_PER_AXIS] = true;
+ }
+
+ // ── Chunk rebuild ─────────────────────────────────────────────────────────
+
+ private void rebuildChunk(int idx) {
+ int chunkX = idx % CHUNKS_PER_AXIS;
+ int chunkZ = idx / CHUNKS_PER_AXIS;
+ Mesh mesh = UpperLayerMesher.buildChunk(data, chunkX, chunkZ);
+
+ if (mesh == null) {
+ // Chunk is fully empty — remove geometry if present
+ if (chunkGeos[idx] != null) {
+ upperNode.detachChild(chunkGeos[idx]);
+ chunkGeos[idx] = null;
+ }
+ } else if (chunkGeos[idx] == null) {
+ Geometry geo = new Geometry("chunk_" + chunkX + "_" + chunkZ, mesh);
+ geo.setMaterial(chunkMat);
+ upperNode.attachChild(geo);
+ chunkGeos[idx] = geo;
+ } else {
+ chunkGeos[idx].setMesh(mesh);
+ }
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.class b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.class
new file mode 100644
index 0000000..11eec49
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.java b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.java
new file mode 100644
index 0000000..b3f18b0
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/ChoiceToolParameter.java
@@ -0,0 +1,33 @@
+package de.blight.editor.tool;
+
+/**
+ * Ein benannter Auswahlparameter eines EditorTools (Enum-artig).
+ * Thread-sicher: JavaFX-Thread schreibt, JME3-Thread liest.
+ */
+public class ChoiceToolParameter {
+
+ private final String name;
+ private final String[] choices;
+ private final String[] imagePaths; // optional, null = ChoiceBox verwenden
+ private volatile int selectedIndex;
+
+ public ChoiceToolParameter(String name, String[] choices, int defaultIndex) {
+ this(name, choices, defaultIndex, null);
+ }
+
+ public ChoiceToolParameter(String name, String[] choices, int defaultIndex, String[] imagePaths) {
+ this.name = name;
+ this.choices = choices;
+ this.imagePaths = imagePaths;
+ this.selectedIndex = defaultIndex;
+ }
+
+ public String getName() { return name; }
+ public String[] getChoices() { return choices; }
+ public String[] getImagePaths() { return imagePaths; }
+ public int getSelectedIndex() { return selectedIndex; }
+
+ public void setSelectedIndex(int i) {
+ if (i >= 0 && i < choices.length) selectedIndex = i;
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.class b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.class
new file mode 100644
index 0000000..54c085d
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.java b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.java
new file mode 100644
index 0000000..ff58b76
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/EditorTool.java
@@ -0,0 +1,21 @@
+package de.blight.editor.tool;
+
+import java.util.List;
+
+/**
+ * Basisklasse für alle Editor-Tools.
+ * Jedes Tool hat einen Namen, Auswahl-Parameter (ChoiceToolParameter)
+ * und numerische Schieberegler-Parameter (ToolParameter).
+ */
+public abstract class EditorTool {
+
+ public abstract String getName();
+
+ /** Numerische Slider-Parameter. */
+ public abstract List getParameters();
+
+ /** Diskrete Auswahl-Parameter (werden als ChoiceBox gerendert). */
+ public List getChoiceParameters() {
+ return List.of();
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.class b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.class
new file mode 100644
index 0000000..55692c1
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java
new file mode 100644
index 0000000..adb803d
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/GrassTool.java
@@ -0,0 +1,21 @@
+package de.blight.editor.tool;
+
+import java.util.List;
+
+/**
+ * Graswerkzeug: Linksklick erhöht Dichte, Rechtsklick verringert sie.
+ */
+public class GrassTool extends EditorTool {
+
+ public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 40.0, 1.0, 500.0);
+ public final ToolParameter grassHeight = new ToolParameter("Grashöhe", 1.5, 0.1, 10.0);
+ public final ToolParameter density = new ToolParameter("Dichte", 8.0, 1.0, 50.0);
+
+ @Override public String getName() { return "Gras"; }
+
+ @Override
+ public List getChoiceParameters() { return List.of(); }
+
+ @Override
+ public List getParameters() { return List.of(brushRadius, grassHeight, density); }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.class b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.class
new file mode 100644
index 0000000..2e92287
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java
new file mode 100644
index 0000000..25dfbcc
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/HeightTool.java
@@ -0,0 +1,43 @@
+package de.blight.editor.tool;
+
+import java.util.List;
+
+/**
+ * Tool zum Anheben und Absenken des Terrains.
+ * Modi: 0=Sinus, 1=Spike, 2=Plateau, 3=Smooth
+ */
+public class HeightTool extends EditorTool {
+
+ public static final int MODE_SINUS = 0;
+ public static final int MODE_SPIKE = 1;
+ public static final int MODE_PLATEAU = 2;
+ public static final int MODE_SMOOTH = 3;
+
+ public final ChoiceToolParameter mode = new ChoiceToolParameter(
+ "Modus",
+ new String[]{"Sinus", "Spike", "Plateau", "Smooth"},
+ MODE_SPIKE,
+ new String[]{
+ "img/editor/terraintool_sinus.png",
+ "img/editor/terraintool_spike.png",
+ "img/editor/terraintool_plateau.png",
+ "img/editor/terraintool_smooth.png"
+ }
+ );
+
+ public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
+ public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
+
+ @Override
+ public String getName() { return "Höhe"; }
+
+ @Override
+ public List getChoiceParameters() {
+ return List.of(mode);
+ }
+
+ @Override
+ public List getParameters() {
+ return List.of(brushRadius, brushStrength);
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class
new file mode 100644
index 0000000..c4b4798
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.java b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.java
new file mode 100644
index 0000000..92bdae4
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/HoleTool.java
@@ -0,0 +1,34 @@
+package de.blight.editor.tool;
+
+import java.util.List;
+
+/**
+ * Tool for digging or filling holes (cave entrances) in the upper layer.
+ * Modes: 0=Dig, 1=Fill
+ */
+public class HoleTool extends EditorTool {
+
+ public static final int MODE_DIG = 0;
+ public static final int MODE_FILL = 1;
+
+ public final ChoiceToolParameter mode = new ChoiceToolParameter(
+ "Modus",
+ new String[]{"Graben", "Füllen"},
+ MODE_DIG
+ );
+
+ public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 20.0, 1.0, 100.0);
+
+ @Override
+ public String getName() { return "Höhlen / Löcher"; }
+
+ @Override
+ public List getChoiceParameters() {
+ return List.of(mode);
+ }
+
+ @Override
+ public List getParameters() {
+ return List.of(brushRadius);
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.class b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.class
new file mode 100644
index 0000000..5663e2b
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java
new file mode 100644
index 0000000..512ee95
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/TextureTool.java
@@ -0,0 +1,30 @@
+package de.blight.editor.tool;
+
+import java.util.List;
+
+/**
+ * Tool zum Bemalen des Basis-Terrains mit Texturen (Splatmap).
+ * Textur-Slots: 0=Gras, 1=Fels, 2=Erde, 3=Sand
+ * Rechtsklick: setzt auf Gras zurück.
+ */
+public class TextureTool extends EditorTool {
+
+ // Terrain.j3md (unlit) hat nur Tex1–Tex3; Slot 0=Gras(Base), 1=Fels(R), 2=Erde(G)
+ public static final String[] TEXTURE_NAMES = {"Gras", "Fels", "Erde"};
+
+ public final ChoiceToolParameter textureIndex = new ChoiceToolParameter(
+ "Textur", TEXTURE_NAMES, 0
+ );
+
+ public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 500.0);
+ public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 0.05, 0.005, 0.5);
+
+ @Override
+ public String getName() { return "Textur"; }
+
+ @Override
+ public List getChoiceParameters() { return List.of(textureIndex); }
+
+ @Override
+ public List getParameters() { return List.of(brushRadius, brushStrength); }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.class b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.class
new file mode 100644
index 0000000..0571b5c
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.java b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.java
new file mode 100644
index 0000000..3710959
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/ToolParameter.java
@@ -0,0 +1,29 @@
+package de.blight.editor.tool;
+
+/**
+ * Ein benannter, numerischer Parameter eines EditorTools.
+ * Thread-sicher: JavaFX-Thread schreibt, JME3-Thread liest.
+ */
+public class ToolParameter {
+
+ private final String name;
+ private final double min;
+ private final double max;
+ private volatile double value;
+
+ public ToolParameter(String name, double defaultValue, double min, double max) {
+ this.name = name;
+ this.min = min;
+ this.max = max;
+ this.value = defaultValue;
+ }
+
+ public String getName() { return name; }
+ public double getMin() { return min; }
+ public double getMax() { return max; }
+ public double getValue() { return value; }
+
+ public void setValue(double v) {
+ this.value = Math.max(min, Math.min(max, v));
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.class b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.class
new file mode 100644
index 0000000..a3da38c
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.java b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.java
new file mode 100644
index 0000000..b02f704
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tool/UpperHeightTool.java
@@ -0,0 +1,37 @@
+package de.blight.editor.tool;
+
+import java.util.List;
+
+/**
+ * Tool for sculpting the top surface of the upper (mountain) layer.
+ * Modes: 0=Raise, 1=Lower, 2=Smooth, 3=Flatten
+ */
+public class UpperHeightTool extends EditorTool {
+
+ public static final int MODE_RAISE = 0;
+ public static final int MODE_LOWER = 1;
+ public static final int MODE_SMOOTH = 2;
+ public static final int MODE_FLATTEN = 3;
+
+ public final ChoiceToolParameter mode = new ChoiceToolParameter(
+ "Modus",
+ new String[]{"Anheben", "Absenken", "Smooth", "Abflachen"},
+ MODE_RAISE
+ );
+
+ public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 50.0, 1.0, 200.0);
+ public final ToolParameter brushStrength = new ToolParameter("Pinselstärke", 2.0, 0.1, 50.0);
+
+ @Override
+ public String getName() { return "Obere Schicht – Höhe"; }
+
+ @Override
+ public List getChoiceParameters() {
+ return List.of(mode);
+ }
+
+ @Override
+ public List getParameters() {
+ return List.of(brushRadius, brushStrength);
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java
new file mode 100644
index 0000000..80e5b88
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java
@@ -0,0 +1,226 @@
+package de.blight.editor.tree;
+
+import com.jme3.math.FastMath;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.util.BufferUtils;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Builds a palm tree Node with two child geometries:
+ * "bark" — tapered trunk cylinder only (palms have no branches)
+ * "leaves" — horizontal bilateral leaflet quads (Wedel/fronds) starting at trunk tip
+ *
+ * Wind weight gradient: 0 near trunk → 1 at leaflet tips, so tips sway most.
+ */
+public class PalmMeshBuilder {
+
+ public static Node build(PalmOptions opts) {
+ Random rng = new Random(opts.seed);
+ float[] azimuths = computeAzimuths(opts, rng);
+ float[] elevations = computeElevations(opts, rng);
+
+ Node palm = new Node("palm");
+ palm.attachChild(new Geometry("bark", buildBarkMesh(opts)));
+ palm.attachChild(new Geometry("leaves", buildLeafMesh(opts, azimuths, elevations)));
+ return palm;
+ }
+
+ // ── Bark mesh: trunk only ─────────────────────────────────────────────────
+
+ private static Mesh buildBarkMesh(PalmOptions opts) {
+ Accum acc = new Accum();
+ addTrunk(acc, opts);
+ return acc.toMesh();
+ }
+
+ private static void addTrunk(Accum acc, PalmOptions opts) {
+ int M = opts.trunkSections;
+ int N = opts.trunkSegments;
+ float H = opts.trunkHeight;
+ float r0 = opts.trunkRadiusBottom;
+ float r1 = opts.trunkRadiusTop;
+
+ int base = acc.vertexCount;
+
+ for (int i = 0; i <= M; i++) {
+ float t = (float) i / M;
+ float r = r0 + (r1 - r0) * t;
+ float y = H * t;
+ float wind = t * 0.4f; // trunk barely sways at root, moderate at crown
+
+ for (int j = 0; j <= N; j++) {
+ float angle = FastMath.TWO_PI * j / N;
+ float nx = FastMath.cos(angle);
+ float nz = FastMath.sin(angle);
+ acc.add(nx * r, y, nz * r, nx, 0f, nz, (float) j / N, t, wind);
+ }
+ }
+
+ for (int i = 0; i < M; i++) {
+ for (int j = 0; j < N; j++) {
+ int r0v = base + i * (N + 1) + j;
+ int r1v = base + (i + 1) * (N + 1) + j;
+ acc.tri(r0v, r1v, r1v + 1);
+ acc.tri(r0v, r1v + 1, r0v + 1);
+ }
+ }
+ }
+
+ // ── Leaf mesh: horizontal bilateral leaflets (Wedel) ─────────────────────
+
+ private static Mesh buildLeafMesh(PalmOptions opts, float[] azimuths, float[] elevations) {
+ Accum acc = new Accum();
+ for (int f = 0; f < opts.frondCount; f++) {
+ addFrondLeaflets(acc, opts, azimuths[f], elevations[f]);
+ }
+ return acc.toMesh();
+ }
+
+ private static void addFrondLeaflets(Accum acc, PalmOptions opts, float azimuth, float elevation) {
+ float sinE = FastMath.sin(elevation);
+ float cosE = FastMath.cos(elevation);
+ float cosA = FastMath.cos(azimuth);
+ float sinA = FastMath.sin(azimuth);
+
+ float dx = sinE * cosA; // frond direction vector
+ float dy = cosE;
+ float dz = sinE * sinA;
+
+ // Horizontal projection for leaflet orientation (keeps leaflets truly flat)
+ float hLen = FastMath.sqrt(dx * dx + dz * dz);
+ if (hLen < 1e-5f) hLen = 1e-5f;
+ float hx = dx / hLen;
+ float hz = dz / hLen;
+
+ // Side direction perpendicular to frond in horizontal plane:
+ // (hx,0,hz) × (0,1,0) = (-hz, 0, hx)
+ float sx = -hz;
+ float sz = hx;
+
+ int K = opts.frondLeafletPairs;
+ float step = opts.frondLength / K;
+ float halfW = step * 0.30f; // leaflet thickness along frond axis
+ float baseY = opts.trunkHeight;
+
+ // Tip width is a fixed fraction of base width — gives natural taper
+ float sizeBase = opts.frondWidth;
+ float sizeTip = opts.frondWidth * 0.18f;
+
+ for (int k = 0; k < K; k++) {
+ float tPos = (float) k / Math.max(1, K - 1); // 0→1 along frond, starts at trunk
+ float leafSize = sizeBase + (sizeTip - sizeBase) * tPos;
+
+ float ax = dx * opts.frondLength * tPos;
+ float ay = baseY + dy * opts.frondLength * tPos;
+ float az = dz * opts.frondLength * tPos;
+
+ // Wind: 0 near trunk tip, more at frond tip; extra delta for outer leaflet edge
+ float windBase = tPos * 0.65f; // inner edge of leaflet
+ float windTip = tPos * 0.65f + 0.35f; // outer edge of leaflet (leaf tip)
+
+ addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, +1f, windBase, windTip);
+ addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, -1f, windBase, windTip);
+ }
+ }
+
+ private static void addLeafletQuad(Accum acc,
+ float ax, float ay, float az,
+ float hx, float hz,
+ float sx, float sz,
+ float leafSize, float halfW, float side,
+ float windInner, float windOuter) {
+ // All 4 vertices at Y = ay (truly horizontal, normal = +Y)
+ float p0x = ax - hx * halfW, p0z = az - hz * halfW;
+ float p1x = ax + hx * halfW, p1z = az + hz * halfW;
+ float p2x = p1x + sx * side * leafSize, p2z = p1z + sz * side * leafSize;
+ float p3x = p0x + sx * side * leafSize, p3z = p0z + sz * side * leafSize;
+
+ int base = acc.vertexCount;
+
+ // p0, p1 = inner edge (attached to frond), p2, p3 = outer tip
+ acc.add(p0x, ay, p0z, 0f, 1f, 0f, 0f, 0f, windInner);
+ acc.add(p1x, ay, p1z, 0f, 1f, 0f, 1f, 0f, windInner);
+ acc.add(p2x, ay, p2z, 0f, 1f, 0f, 1f, 1f, windOuter);
+ acc.add(p3x, ay, p3z, 0f, 1f, 0f, 0f, 1f, windOuter);
+
+ acc.tri(base, base + 1, base + 2);
+ acc.tri(base, base + 2, base + 3);
+ }
+
+ // ── Random frond placement ────────────────────────────────────────────────
+
+ private static float[] computeAzimuths(PalmOptions opts, Random rng) {
+ float[] a = new float[opts.frondCount];
+ for (int f = 0; f < opts.frondCount; f++) {
+ a[f] = FastMath.TWO_PI * f / opts.frondCount + (rng.nextFloat() - 0.5f) * 0.4f;
+ }
+ return a;
+ }
+
+ private static float[] computeElevations(PalmOptions opts, Random rng) {
+ float minR = opts.frondAngleMin * FastMath.DEG_TO_RAD;
+ float maxR = opts.frondAngleMax * FastMath.DEG_TO_RAD;
+ float[] a = new float[opts.frondCount];
+ for (int f = 0; f < opts.frondCount; f++) {
+ a[f] = minR + rng.nextFloat() * (maxR - minR);
+ }
+ return a;
+ }
+
+ // ── Vertex accumulator ────────────────────────────────────────────────────
+
+ private static final class Accum {
+ final List pos = new ArrayList<>();
+ final List norm = new ArrayList<>();
+ final List uv = new ArrayList<>();
+ final List col = new ArrayList<>();
+ final List idx = new ArrayList<>();
+ int vertexCount = 0;
+
+ void add(float x, float y, float z,
+ float nx, float ny, float nz,
+ float u, float v, float wind) {
+ pos.add(x); pos.add(y); pos.add(z);
+ norm.add(nx); norm.add(ny); norm.add(nz);
+ uv.add(u); uv.add(v);
+ col.add(wind); col.add(0f); col.add(0f); col.add(1f);
+ vertexCount++;
+ }
+
+ void tri(int a, int b, int c) { idx.add(a); idx.add(b); idx.add(c); }
+
+ Mesh toMesh() {
+ if (vertexCount == 0) return new Mesh();
+ int n = vertexCount;
+
+ FloatBuffer posB = BufferUtils.createFloatBuffer(n * 3);
+ FloatBuffer normB = BufferUtils.createFloatBuffer(n * 3);
+ FloatBuffer uvB = BufferUtils.createFloatBuffer(n * 2);
+ FloatBuffer colB = BufferUtils.createFloatBuffer(n * 4);
+ IntBuffer idxB = BufferUtils.createIntBuffer(idx.size());
+
+ for (Float f : pos) posB.put(f);
+ for (Float f : norm) normB.put(f);
+ for (Float f : uv) uvB.put(f);
+ for (Float f : col) colB.put(f);
+ for (Integer i : idx) idxB.put(i);
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, posB);
+ mesh.setBuffer(VertexBuffer.Type.Normal, 3, normB);
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uvB);
+ mesh.setBuffer(VertexBuffer.Type.Color, 4, colB);
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, idxB);
+ mesh.updateBound();
+ return mesh;
+ }
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java
new file mode 100644
index 0000000..90734f3
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java
@@ -0,0 +1,50 @@
+package de.blight.editor.tree;
+
+public class PalmOptions {
+
+ public int seed = 42;
+
+ // Trunk
+ public float trunkHeight = 12f;
+ public float trunkRadiusBottom = 0.35f;
+ public float trunkRadiusTop = 0.30f; // tapers but stays thick
+ public int trunkSections = 10;
+ public int trunkSegments = 8;
+
+ // Fronds
+ public int frondCount = 10;
+ public float frondAngleMin = 70f; // degrees from vertical (Y-up)
+ public float frondAngleMax = 110f;
+ public float frondLength = 6.5f;
+ public int frondLeafletPairs = 8;
+ public float frondWidth = 1.4f; // max leaflet width at frond base
+
+ // Colors
+ public float barkR = 0.68f, barkG = 0.54f, barkB = 0.34f;
+ public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f;
+
+ // Textures
+ public String barkTexture = "Textures/bark/Bark008_Color.jpg";
+ public String leafTexture = "Textures/leaves/palm.png";
+
+ public PalmOptions copy() {
+ PalmOptions c = new PalmOptions();
+ c.seed = seed;
+ c.trunkHeight = trunkHeight;
+ c.trunkRadiusBottom = trunkRadiusBottom;
+ c.trunkRadiusTop = trunkRadiusTop;
+ c.trunkSections = trunkSections;
+ c.trunkSegments = trunkSegments;
+ c.frondCount = frondCount;
+ c.frondAngleMin = frondAngleMin;
+ c.frondAngleMax = frondAngleMax;
+ c.frondLength = frondLength;
+ c.frondLeafletPairs = frondLeafletPairs;
+ c.frondWidth = frondWidth;
+ c.barkR = barkR; c.barkG = barkG; c.barkB = barkB;
+ c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
+ c.barkTexture = barkTexture;
+ c.leafTexture = leafTexture;
+ return c;
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog$FloatSetter.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog$FloatSetter.class
new file mode 100644
index 0000000..3e7662e
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog$FloatSetter.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog.class
new file mode 100644
index 0000000..52b04ee
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeGeneratorDialog.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$1BranchTask.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$1BranchTask.class
new file mode 100644
index 0000000..03f4872
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$1BranchTask.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$MeshResult.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$MeshResult.class
new file mode 100644
index 0000000..72bf60c
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$MeshResult.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$VertexCollector.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$VertexCollector.class
new file mode 100644
index 0000000..1727b04
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder$VertexCollector.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.class
new file mode 100644
index 0000000..cc890eb
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java
new file mode 100644
index 0000000..946221d
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java
@@ -0,0 +1,359 @@
+package de.blight.editor.tree;
+
+import com.jme3.bounding.BoundingBox;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.util.BufferUtils;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.*;
+
+/**
+ * Prozeduraler Baum-Generator nach dem ez-tree-Ansatz:
+ * • Jeder Ast wird in mehrere Sektionen unterteilt (organische Kurven)
+ * • Gnarliness-Perturbation: dünne Äste wackeln stärker
+ * • Gravitation: Äste hängen proportional zur Kraft nach unten (negativ = aufwärts)
+ * • Stratifizierte Kind-Platzierung: verhindert Clustering
+ * • MWC-RNG für reproduzierbare Ergebnisse mit beliebigem Seed
+ *
+ * quality 1.0 = HD (volle Sektionen/Segmente), 0.0 = LD (halbe Sektionen, -2 Segmente)
+ */
+public class TreeMeshBuilder {
+
+ public record MeshResult(Mesh bark, Mesh leaves, BoundingBox bounds) {}
+
+ // ── Haupt-Einstieg ────────────────────────────────────────────────────────
+
+ public MeshResult build(TreeParams p, float quality) {
+ boolean hd = quality >= 0.5f;
+ int maxLevel = hd ? p.levels : Math.max(1, p.levels - 1);
+
+ Rng rng = new Rng(p.seed);
+ VertexCollector barkCol = new VertexCollector();
+ VertexCollector leafCol = new VertexCollector();
+
+ record BranchTask(Vector3f origin, Vector3f dir, int level, float windBase) {}
+ record SectionPt (Vector3f pos, Vector3f dir, float wind) {}
+
+ Deque stack = new ArrayDeque<>();
+ stack.push(new BranchTask(new Vector3f(0, 0, 0), new Vector3f(0, 1, 0),
+ 0, p.trunkFlexibility));
+
+ while (!stack.isEmpty()) {
+ BranchTask t = stack.pop();
+ int lv = t.level();
+ if (lv >= maxLevel) continue;
+
+ // Per-Level-Parameter
+ int numSec = Math.max(1, hd ? TreeParams.lv(p.sections, lv)
+ : TreeParams.lv(p.sections, lv) / 2);
+ float branchLen = TreeParams.lv(p.length, lv);
+ float baseRad = TreeParams.lv(p.radius, lv);
+ float tapFactor = TreeParams.lv(p.taper, lv);
+ float gnarl = TreeParams.lv(p.gnarliness,lv);
+ int segs = Math.max(3, TreeParams.lv(p.segments, lv) - (hd ? 0 : 2));
+ float segLen = branchLen / numSec;
+
+ // Wind-Bereich für diesen Ast
+ float windEnd = p.trunkFlexibility
+ + (p.branchFlexibility - p.trunkFlexibility)
+ * (float)(lv + 1) / p.levels;
+ windEnd = FastMath.clamp(Math.max(t.windBase(), windEnd), 0f, 1f);
+
+ Vector3f pos = t.origin().clone();
+ Vector3f dir = t.dir().clone();
+ List sectionPts = new ArrayList<>(numSec);
+
+ for (int s = 0; s < numSec; s++) {
+ float rBot = baseRad * lerp(1f, tapFactor, (float) s / numSec);
+ float rTop = baseRad * lerp(1f, tapFactor, (float)(s + 1) / numSec);
+ float wBot = lerp(t.windBase(), windEnd, (float) s / numSec);
+ float wTop = lerp(t.windBase(), windEnd, (float)(s + 1) / numSec);
+
+ // Gnarliness: dünne Äste stärker perturbieren (nur ab Level 1)
+ if (lv > 0 && gnarl > 1e-4f) {
+ float g = gnarl / (float) Math.sqrt(Math.max(0.01f, rBot));
+ dir.x += rng.range(-g, g);
+ dir.z += rng.range(-g, g);
+ dir.normalizeLocal();
+ }
+
+ // Gravitation: Richtung graduell zur Gravitations-Richtung drehen
+ applyGravity(dir, rBot, p.gravityStrength);
+
+ Vector3f end = pos.add(dir.mult(segLen));
+ addCylinder(barkCol, pos, end, rBot, rTop, wBot, wTop, segs);
+ sectionPts.add(new SectionPt(pos.clone(), dir.clone(), wTop));
+ pos = end;
+ }
+
+ if (lv < maxLevel - 1) {
+ // Kind-Äste stratifiziert entlang des Eltern-Astes verteilen
+ int nChildren = TreeParams.lv(p.children, lv);
+ float childStart = TreeParams.lv(p.start, lv + 1); // Startfraktion
+
+ for (int k = 0; k < nChildren; k++) {
+ float frac = childStart + (1f - childStart)
+ * (k + rng.range(-0.35f, 0.35f)) / nChildren;
+ frac = FastMath.clamp(frac, childStart, 1f);
+ int si = Math.min((int)(frac * sectionPts.size()), sectionPts.size() - 1);
+ SectionPt sp = sectionPts.get(si);
+
+ float yRot = k * FastMath.TWO_PI / nChildren + rng.range(-0.4f, 0.4f);
+ float nextAngle = TreeParams.lv(p.angle, lv + 1);
+ stack.push(new BranchTask(
+ sp.pos(), branchDir(sp.dir(), yRot, nextAngle),
+ lv + 1, sp.wind()));
+ }
+ } else if (p.generateLeaves) {
+ // Blätter entlang des gesamten letzten Astes (jede Sektion)
+ int sectionLeafCount = Math.max(1, p.leafCount * 2 / 3);
+ for (SectionPt sp : sectionPts) {
+ addLeafCluster(leafCol, sp.pos(), sp.wind(),
+ p.leafScale * 0.75f, sectionLeafCount, p.leafAngle, rng);
+ }
+ // Blatt-Cluster an der Spitze (Originalgröße)
+ addLeafCluster(leafCol, pos, windEnd, p.leafScale, p.leafCount, p.leafAngle, rng);
+
+ // Seiten-Zweige am letzten Ast (leafBranchings, 0–3)
+ if (p.leafBranchings > 0) {
+ float twigLen = branchLen * 0.4f;
+ float twigRad = baseRad * 0.45f;
+ float twigAngle = TreeParams.lv(p.angle, lv) * 0.75f;
+ for (SectionPt sp : sectionPts) {
+ for (int b = 0; b < p.leafBranchings; b++) {
+ float yRot = b * FastMath.TWO_PI / p.leafBranchings
+ + rng.range(-0.5f, 0.5f);
+ Vector3f twigDir = branchDir(sp.dir(), yRot, twigAngle);
+ Vector3f twigEnd = sp.pos().add(twigDir.mult(twigLen));
+ addCylinder(barkCol, sp.pos(), twigEnd,
+ twigRad, twigRad * 0.35f,
+ sp.wind(), windEnd, Math.max(3, segs - 1));
+ addLeafCluster(leafCol, twigEnd, windEnd,
+ p.leafScale, p.leafCount, p.leafAngle, rng);
+ }
+ }
+ }
+ }
+ }
+
+ return new MeshResult(barkCol.toMesh(), leafCol.toMesh(), computeBounds(barkCol));
+ }
+
+ // ── Gravitations-Kraft ────────────────────────────────────────────────────
+
+ private static void applyGravity(Vector3f dir, float radius, float strength) {
+ if (Math.abs(strength) < 1e-5f) return;
+ // strength > 0 → nach unten ziehen; strength < 0 → nach oben ziehen
+ Vector3f target = new Vector3f(0, strength > 0 ? -1f : 1f, 0);
+ Vector3f axis = dir.cross(target);
+ float sinFull = axis.length();
+ if (sinFull < 1e-6f) return;
+ axis.divideLocal(sinFull);
+ float fullAngle = FastMath.atan2(sinFull, dir.dot(target));
+ float step = Math.abs(strength) / Math.max(0.01f, radius);
+ float clamped = FastMath.clamp(step, 0f, Math.abs(fullAngle));
+ new Quaternion().fromAngleAxis(clamped, axis).multLocal(dir);
+ dir.normalizeLocal();
+ }
+
+ // ── Hilfsmethoden ────────────────────────────────────────────────────────
+
+ private static float lerp(float a, float b, float t) { return a + (b - a) * t; }
+
+ // ── Zylinder-Segment ─────────────────────────────────────────────────────
+
+ private static void addCylinder(VertexCollector col,
+ Vector3f start, Vector3f end,
+ float rBot, float rTop,
+ float windBot, float windTop,
+ int N) {
+ Vector3f axis = end.subtract(start);
+ if (axis.lengthSquared() < 1e-8f) return;
+ axis.normalizeLocal();
+
+ Vector3f perp1 = (Math.abs(axis.y) < 0.9f)
+ ? axis.cross(Vector3f.UNIT_Y).normalizeLocal()
+ : axis.cross(Vector3f.UNIT_X).normalizeLocal();
+ Vector3f perp2 = axis.cross(perp1).normalizeLocal();
+
+ int base = col.vertexCount;
+ int N1 = N + 1;
+
+ for (int ring = 0; ring < 2; ring++) {
+ Vector3f center = (ring == 0) ? start : end;
+ float r = (ring == 0) ? rBot : rTop;
+ float wind = (ring == 0) ? windBot : windTop;
+ float vCoord = ring;
+ for (int i = 0; i <= N; i++) {
+ float theta = FastMath.TWO_PI * i / N;
+ float cos = FastMath.cos(theta);
+ float sin = FastMath.sin(theta);
+ float nx = cos * perp1.x + sin * perp2.x;
+ float ny = cos * perp1.y + sin * perp2.y;
+ float nz = cos * perp1.z + sin * perp2.z;
+ col.add(center.x + nx * r, center.y + ny * r, center.z + nz * r,
+ nx, ny, nz,
+ (float) i / N, vCoord, wind);
+ }
+ }
+
+ for (int i = 0; i < N; i++) {
+ int b0 = base + i, b1 = base + i + 1;
+ int t0 = base + N1 + i, t1 = base + N1 + i + 1;
+ col.tri(b0, b1, t1);
+ col.tri(b0, t1, t0);
+ }
+ }
+
+ // ── Blatt-Cluster ────────────────────────────────────────────────────────
+
+ private static void addLeafCluster(VertexCollector col, Vector3f tip,
+ float wind, float scale, int count,
+ float angleDeg, Rng rng) {
+ for (int i = 0; i < count; i++) {
+ float ox = rng.range(-scale * 0.5f, scale * 0.5f);
+ float oy = rng.range(-scale * 0.25f, scale * 0.25f);
+ float oz = rng.range(-scale * 0.5f, scale * 0.5f);
+ float s = scale * (0.7f + rng.range(0f, 0.6f));
+ float tilt = angleDeg * FastMath.DEG_TO_RAD;
+ addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz,
+ s, wind, rng.range(0f, FastMath.TWO_PI), tilt);
+ }
+ }
+
+ private static void addLeafQuad(VertexCollector col,
+ float cx, float cy, float cz,
+ float s, float wind, float yRot, float tilt) {
+ float cosY = FastMath.cos(yRot), sinY = FastMath.sin(yRot);
+ float cosT = FastMath.cos(tilt), sinT = FastMath.sin(tilt);
+ float hw = s * 0.5f;
+ float hh = s * 0.65f;
+
+ // Quad A
+ for (int q = 0; q < 2; q++) {
+ // second quad perpendicular (yRot + PI/2)
+ float cy2 = (q == 0) ? cosY : -sinY;
+ float sz2 = (q == 0) ? sinY : cosY;
+ int base = col.vertexCount;
+ // 4 Ecken: unten-links, unten-rechts, oben-rechts, oben-links
+ // "oben" ist um tilt-Grad nach vorne/hinten geneigt
+ float[] xs = {-hw * cy2, hw * cy2, hw * cy2, -hw * cy2};
+ float[] zs = {-hw * sz2, hw * sz2, hw * sz2, -hw * sz2};
+ float[] ys = {-hh * cosT, -hh * cosT, hh * cosT, hh * cosT};
+
+ // Face normal = right × up = (cy2,0,sz2) × (0,1,0) = (-sz2, 0, cy2)
+ float nnx = -sz2, nnz = cy2;
+ col.add(cx + xs[0], cy + ys[0], cz + zs[0], nnx, 0, nnz, 0, 0, wind);
+ col.add(cx + xs[1], cy + ys[1], cz + zs[1], nnx, 0, nnz, 1, 0, wind);
+ col.add(cx + xs[2], cy + ys[2], cz + zs[2], nnx, 0, nnz, 1, 1, wind);
+ col.add(cx + xs[3], cy + ys[3], cz + zs[3], nnx, 0, nnz, 0, 1, wind);
+ col.tri(base, base+1, base+2);
+ col.tri(base, base+2, base+3);
+ col.tri(base+2, base+1, base); // back face
+ col.tri(base+3, base+2, base);
+ }
+ }
+
+ // ── Ast-Richtung berechnen ────────────────────────────────────────────────
+
+ private static Vector3f branchDir(Vector3f parent, float yRot, float tiltDeg) {
+ Vector3f perp = (Math.abs(parent.y) < 0.9f)
+ ? parent.cross(Vector3f.UNIT_Y).normalizeLocal()
+ : parent.cross(Vector3f.UNIT_X).normalizeLocal();
+ Quaternion tilt = new Quaternion().fromAngleAxis(tiltDeg * FastMath.DEG_TO_RAD, perp);
+ Quaternion spin = new Quaternion().fromAngleAxis(yRot, parent);
+ return spin.mult(tilt.mult(parent)).normalizeLocal();
+ }
+
+ // ── BoundingBox ───────────────────────────────────────────────────────────
+
+ private static BoundingBox computeBounds(VertexCollector col) {
+ if (col.pos.isEmpty()) return new BoundingBox();
+ float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE, minZ = Float.MAX_VALUE;
+ float maxX = -Float.MAX_VALUE, maxY = -Float.MAX_VALUE, maxZ = -Float.MAX_VALUE;
+ for (int i = 0; i < col.pos.size(); i += 3) {
+ float x = col.pos.get(i), y = col.pos.get(i+1), z = col.pos.get(i+2);
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
+ if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
+ }
+ return new BoundingBox(
+ new Vector3f((minX+maxX)*0.5f, (minY+maxY)*0.5f, (minZ+maxZ)*0.5f),
+ (maxX-minX)*0.5f, (maxY-minY)*0.5f, (maxZ-minZ)*0.5f);
+ }
+
+ // ── MWC-RNG (portiert aus ez-tree/rng.js) ────────────────────────────────
+
+ private static final class Rng {
+ private long w, z;
+
+ Rng(int seed) {
+ w = (123456789L + seed) & 0xFFFFFFFFL;
+ z = (987654321L - seed) & 0xFFFFFFFFL;
+ }
+
+ /** Gleichverteilte Zufallszahl in [0, 1). */
+ float next() {
+ z = (36969L * (z & 65535L) + (z >> 16)) & 0xFFFFFFFFL;
+ w = (18000L * (w & 65535L) + (w >> 16)) & 0xFFFFFFFFL;
+ long result = ((z << 16) + (w & 65535L)) & 0xFFFFFFFFL;
+ return (float) result / 4294967296f;
+ }
+
+ float range(float lo, float hi) { return lo + (hi - lo) * next(); }
+ }
+
+ // ── Vertex-Sammler ────────────────────────────────────────────────────────
+
+ private static final class VertexCollector {
+ final List pos = new ArrayList<>();
+ final List norm = new ArrayList<>();
+ final List uv = new ArrayList<>();
+ final List col = new ArrayList<>();
+ final List idx = new ArrayList<>();
+ int vertexCount = 0;
+
+ void add(float x, float y, float z,
+ float nx, float ny, float nz,
+ float u, float v, float wind) {
+ pos.add(x); pos.add(y); pos.add(z);
+ norm.add(nx); norm.add(ny); norm.add(nz);
+ uv.add(u); uv.add(v);
+ col.add(wind); col.add(0f); col.add(0f); col.add(1f);
+ vertexCount++;
+ }
+
+ void tri(int a, int b, int c) { idx.add(a); idx.add(b); idx.add(c); }
+
+ Mesh toMesh() {
+ if (vertexCount == 0) return new Mesh();
+ int n = vertexCount;
+
+ FloatBuffer posB = BufferUtils.createFloatBuffer(n * 3);
+ FloatBuffer normB = BufferUtils.createFloatBuffer(n * 3);
+ FloatBuffer uvB = BufferUtils.createFloatBuffer(n * 2);
+ FloatBuffer colB = BufferUtils.createFloatBuffer(n * 4);
+ IntBuffer idxB = BufferUtils.createIntBuffer(idx.size());
+
+ for (Float f : pos) posB.put(f);
+ for (Float f : norm) normB.put(f);
+ for (Float f : uv) uvB.put(f);
+ for (Float f : col) colB.put(f);
+ for (Integer i : idx) idxB.put(i);
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, posB);
+ mesh.setBuffer(VertexBuffer.Type.Normal, 3, normB);
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uvB);
+ mesh.setBuffer(VertexBuffer.Type.Color, 4, colB);
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, idxB);
+ mesh.updateBound();
+ return mesh;
+ }
+ }
+}
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.class b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.class
new file mode 100644
index 0000000..352ab86
Binary files /dev/null and b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.class differ
diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java
new file mode 100644
index 0000000..3b3258a
--- /dev/null
+++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java
@@ -0,0 +1,194 @@
+package de.blight.editor.tree;
+
+/**
+ * Parameter für den prozeduralen Baum-Generator.
+ *
+ * Per-Level-Arrays (Index 0 = Stamm, 1 = Hauptäste, 2 = Sekundäräste, 3 = Endäste).
+ * Inspiriert von ez-tree (github.com/dgreenheck/ez-tree).
+ */
+public class TreeParams {
+
+ // ── Global ────────────────────────────────────────────────────────────────
+ public int seed = 42;
+ public int levels = 3; // Rekursionstiefe (1–4)
+ public float gravityStrength = 0.03f; // >0 = Äste hängen, <0 = aufwärts (Tanne)
+
+ // ── Per-Level-Arrays (Index = Level) ──────────────────────────────────────
+ /** Astwinkel zur Eltern-Richtung in Grad (Level 0 = Stamm, ignoriert). */
+ public float[] angle = { 0f, 55f, 45f, 30f };
+ /** Anzahl Kind-Äste, die ein Level-L-Ast erzeugt (bei Level-L = tiefstem Level → Blätter). */
+ public int[] children = { 5, 4, 3, 0 };
+ /** Ab welchem Anteil der Eltern-Astlänge Kinder beginnen (0 = ganz unten, 0.4 = ab 40%). */
+ public float[] start = { 0.0f, 0.35f, 0.25f, 0.10f };
+ /** Länge eines Astes auf diesem Level in Welteinheiten. */
+ public float[] length = { 14f, 10f, 7f, 1.5f};
+ /** Startradius eines Astes auf diesem Level. */
+ public float[] radius = { 0.40f, 0.18f, 0.10f, 0.06f };
+ /** Anzahl Sektionen pro Ast (mehr = organischerer Kurvenverlauf). */
+ public int[] sections = { 8, 6, 4, 2 };
+ /** Radiale Segmente pro Sektion (Zylinder-Querschnitt). */
+ public int[] segments = { 8, 6, 4, 3 };
+ /** Radius-Abnahme-Faktor pro Sektion (0.5–1.0). */
+ public float[] taper = { 0.70f, 0.65f, 0.65f, 0.70f };
+ /** Zufällige Richtungsabweichung; dünne Äste wackeln stärker. */
+ public float[] gnarliness= { 0.00f, 0.10f, 0.20f, 0.05f };
+
+ // ── Blätter ───────────────────────────────────────────────────────────────
+ public boolean generateLeaves = true;
+ public float leafScale = 1.2f;
+ public int leafCount = 5;
+ public float leafAngle = 45f;
+ /** Anzahl kleiner Seiten-Zweige am letzten Ast (0 = keine, 3 = dicht belaubt). */
+ public int leafBranchings = 1;
+
+ // ── Texturen (relative Pfade für den Asset-Manager) ──────────────────────
+ public String barkTexture = null; // z.B. "Textures/bark/Bark001_Color.jpg"
+ public String leafTexture = null; // z.B. "Textures/leaves/oak.png"
+
+ // ── Wind ─────────────────────────────────────────────────────────────────
+ public float trunkFlexibility = 0.05f;
+ public float branchFlexibility = 0.90f;
+
+ // ── Presets ───────────────────────────────────────────────────────────────
+
+ public static TreeParams oak() {
+ TreeParams p = new TreeParams();
+ p.seed = 35729;
+ p.levels = 3;
+ p.gravityStrength = 0.04f;
+ p.angle = new float[]{ 0f, 54f, 58f, 32f };
+ p.children = new int[] { 6, 4, 3, 0 };
+ p.start = new float[]{ 0.0f, 0.35f, 0.20f, 0.10f };
+ p.length = new float[]{ 14f, 11f, 8f, 1.5f};
+ p.radius = new float[]{ 0.45f, 0.20f, 0.11f, 0.06f };
+ p.sections = new int[] { 8, 6, 4, 2 };
+ p.segments = new int[] { 8, 6, 4, 3 };
+ p.taper = new float[]{ 0.73f, 0.65f, 0.69f, 0.75f };
+ p.gnarliness= new float[]{ 0.00f, 0.10f, 0.15f, 0.09f };
+ p.leafScale = 1.4f; p.leafCount = 6; p.leafAngle = 42f; p.leafBranchings = 2;
+ p.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f;
+ p.barkTexture = "Textures/bark/Bark001_Color.jpg";
+ p.leafTexture = "Textures/leaves/oak.png";
+ return p;
+ }
+
+ public static TreeParams birch() {
+ TreeParams p = new TreeParams();
+ p.seed = 11204;
+ p.levels = 3;
+ p.gravityStrength = 0.01f;
+ p.angle = new float[]{ 0f, 45f, 40f, 25f };
+ p.children = new int[] { 4, 3, 3, 0 };
+ p.start = new float[]{ 0.0f, 0.45f, 0.30f, 0.10f };
+ p.length = new float[]{ 18f, 12f, 6f, 1.2f};
+ p.radius = new float[]{ 0.30f, 0.12f, 0.07f, 0.04f };
+ p.sections = new int[] { 10, 7, 4, 2 };
+ p.segments = new int[] { 7, 5, 4, 3 };
+ p.taper = new float[]{ 0.68f, 0.60f, 0.62f, 0.70f };
+ p.gnarliness= new float[]{ 0.00f, 0.05f, 0.10f, 0.04f };
+ p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1;
+ p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f;
+ p.barkTexture = "Textures/bark/Bark002_Color.jpg";
+ p.leafTexture = "Textures/leaves/aspen.png";
+ return p;
+ }
+
+ public static TreeParams pine() {
+ TreeParams p = new TreeParams();
+ p.seed = 72831;
+ p.levels = 3;
+ p.gravityStrength = -0.015f;
+ p.angle = new float[]{ 0f, 75f, 60f, 40f };
+ p.children = new int[] { 7, 5, 4, 0 };
+ p.start = new float[]{ 0.0f, 0.15f, 0.20f, 0.10f };
+ p.length = new float[]{ 12f, 7f, 4f, 0.8f};
+ p.radius = new float[]{ 0.35f, 0.12f, 0.07f, 0.04f };
+ p.sections = new int[] { 8, 5, 3, 2 };
+ p.segments = new int[] { 7, 5, 4, 3 };
+ p.taper = new float[]{ 0.65f, 0.58f, 0.60f, 0.65f };
+ p.gnarliness= new float[]{ 0.00f, 0.03f, 0.08f, 0.02f };
+ p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1;
+ p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f;
+ p.barkTexture = "Textures/bark/Bark003_Color.jpg";
+ p.leafTexture = "Textures/leaves/pine.png";
+ return p;
+ }
+
+ public static TreeParams willow() {
+ TreeParams p = new TreeParams();
+ p.seed = 54321;
+ p.levels = 3;
+ p.gravityStrength = 0.12f;
+ p.angle = new float[]{ 0f, 60f, 50f, 35f };
+ p.children = new int[] { 5, 4, 3, 0 };
+ p.start = new float[]{ 0.0f, 0.30f, 0.20f, 0.10f };
+ p.length = new float[]{ 10f, 12f, 8f, 2.0f};
+ p.radius = new float[]{ 0.38f, 0.16f, 0.09f, 0.05f };
+ p.sections = new int[] { 8, 8, 5, 3 };
+ p.segments = new int[] { 7, 5, 4, 3 };
+ p.taper = new float[]{ 0.72f, 0.68f, 0.65f, 0.72f };
+ p.gnarliness= new float[]{ 0.00f, 0.25f, 0.35f, 0.15f };
+ p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2;
+ p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f;
+ p.barkTexture = "Textures/bark/Bark001_Color.jpg";
+ p.leafTexture = "Textures/leaves/ash.png";
+ return p;
+ }
+
+ public static TreeParams bush() {
+ TreeParams p = new TreeParams();
+ p.seed = 9876;
+ p.levels = 2;
+ p.gravityStrength = 0.02f;
+ p.angle = new float[]{ 0f, 65f, 55f, 40f };
+ p.children = new int[] { 8, 5, 0, 0 };
+ p.start = new float[]{ 0.0f, 0.10f, 0.15f, 0.10f };
+ p.length = new float[]{ 2f, 2f, 1.2f, 0.5f};
+ p.radius = new float[]{ 0.18f, 0.08f, 0.05f, 0.03f };
+ p.sections = new int[] { 4, 4, 3, 2 };
+ p.segments = new int[] { 6, 5, 4, 3 };
+ p.taper = new float[]{ 0.65f, 0.60f, 0.62f, 0.65f };
+ p.gnarliness= new float[]{ 0.00f, 0.15f, 0.25f, 0.10f };
+ p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2;
+ p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f;
+ p.barkTexture = "Textures/bark/Bark001_Color.jpg";
+ p.leafTexture = "Textures/leaves/ash.png";
+ return p;
+ }
+
+ // ── Hilfsmethoden ─────────────────────────────────────────────────────────
+
+ public TreeParams copy() {
+ TreeParams c = new TreeParams();
+ c.seed = seed;
+ c.levels = levels;
+ c.gravityStrength = gravityStrength;
+ c.angle = angle.clone();
+ c.children = children.clone();
+ c.start = start.clone();
+ c.length = length.clone();
+ c.radius = radius.clone();
+ c.sections = sections.clone();
+ c.segments = segments.clone();
+ c.taper = taper.clone();
+ c.gnarliness = gnarliness.clone();
+ c.generateLeaves = generateLeaves;
+ c.leafScale = leafScale;
+ c.leafCount = leafCount;
+ c.leafAngle = leafAngle;
+ c.leafBranchings = leafBranchings;
+ c.trunkFlexibility = trunkFlexibility;
+ c.branchFlexibility = branchFlexibility;
+ c.barkTexture = barkTexture;
+ c.leafTexture = leafTexture;
+ return c;
+ }
+
+ /** Liefert arr[level], bei Überlauf den letzten Wert. */
+ public static float lv(float[] arr, int level) {
+ return arr[Math.min(level, arr.length - 1)];
+ }
+ public static int lv(int[] arr, int level) {
+ return arr[Math.min(level, arr.length - 1)];
+ }
+}
diff --git a/blight-editor/src/main/resources/img/editor/grasstool.png b/blight-editor/src/main/resources/img/editor/grasstool.png
new file mode 100644
index 0000000..7f581fe
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/grasstool.png differ
diff --git a/blight-editor/src/main/resources/img/editor/terraintool.png b/blight-editor/src/main/resources/img/editor/terraintool.png
new file mode 100644
index 0000000..f33bc63
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool.png differ
diff --git a/blight-editor/src/main/resources/img/editor/terraintool_plateau.png b/blight-editor/src/main/resources/img/editor/terraintool_plateau.png
new file mode 100644
index 0000000..f3f8e1c
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_plateau.png differ
diff --git a/blight-editor/src/main/resources/img/editor/terraintool_sinus.png b/blight-editor/src/main/resources/img/editor/terraintool_sinus.png
new file mode 100644
index 0000000..bb8dfc6
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_sinus.png differ
diff --git a/blight-editor/src/main/resources/img/editor/terraintool_smooth.png b/blight-editor/src/main/resources/img/editor/terraintool_smooth.png
new file mode 100644
index 0000000..2eff4bd
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_smooth.png differ
diff --git a/blight-editor/src/main/resources/img/editor/terraintool_spike.png b/blight-editor/src/main/resources/img/editor/terraintool_spike.png
new file mode 100644
index 0000000..88f6c67
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/terraintool_spike.png differ
diff --git a/blight-editor/src/main/resources/img/editor/textruretool.png b/blight-editor/src/main/resources/img/editor/textruretool.png
new file mode 100644
index 0000000..11e4518
Binary files /dev/null and b/blight-editor/src/main/resources/img/editor/textruretool.png differ
diff --git a/blight-editor/world/blight_map.blm b/blight-editor/world/blight_map.blm
new file mode 100644
index 0000000..8b5dd3c
Binary files /dev/null and b/blight-editor/world/blight_map.blm differ
diff --git a/blight-game/.gitignore b/blight-game/.gitignore
new file mode 100644
index 0000000..da88288
--- /dev/null
+++ b/blight-game/.gitignore
@@ -0,0 +1 @@
+/.gradle/
diff --git a/blight-game/bin/main/Textures/gras.png b/blight-game/bin/main/Textures/gras.png
deleted file mode 100644
index 2f131cb..0000000
Binary files a/blight-game/bin/main/Textures/gras.png and /dev/null differ
diff --git a/blight-game/build.gradle b/blight-game/build.gradle
index 6caa0d4..b78f1b1 100644
--- a/blight-game/build.gradle
+++ b/blight-game/build.gradle
@@ -1,81 +1,53 @@
-plugins {
- id 'java'
- id 'application'
-}
-
-group = 'de.blight'
-version = '0.1.0'
-
-java {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
-}
-
-sourceSets {
- main {
- resources {
- srcDirs = ['src/main/resources', 'assets']
- }
- }
-}
-
-application {
- mainClass = 'de.blight.game.BlightApp'
- // jMonkeyEngine benötigt den headless-Flag nicht; nativer Fenstermodus
- applicationDefaultJvmArgs = [
- '--add-opens', 'java.base/java.lang=ALL-UNNAMED',
- '--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',
- '-Djava.library.path=${rootDir}/build/natives'
- ]
-}
-
-repositories {
- mavenCentral()
-}
-
-ext {
- jmeVersion = '3.7.0-stable'
-}
-
-dependencies {
- // jMonkeyEngine core
- implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
-
- // Terrain, Effects, Plugins
- implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
- implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
-
- // Bullet-Physik (nativ)
- implementation "org.jmonkeyengine:jme3-jbullet:${jmeVersion}"
-
- // Testdaten / eingebaute Assets (Primitiv-Modelle, Skybox etc.)
- implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
-
- // JSON für Key-Binding-Konfiguration
- implementation 'com.google.code.gson:gson:2.11.0'
-}
-
-// Native Libraries automatisch entpacken
-tasks.register('extractNatives', Copy) {
- def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
- .resolvedArtifacts
- .findAll { it.name.contains('natives') }
- .collect { zipTree(it.file) }
-
- from nativeConf
- into "${buildDir}/natives"
- duplicatesStrategy = DuplicatesStrategy.INCLUDE
-}
-
-run {
- dependsOn extractNatives
- workingDir = rootDir
-}
-
-jar {
- manifest {
- attributes 'Main-Class': application.mainClass
- }
-}
+// group / version / java / repositories kommen vom Root-Build.
+plugins {
+ id 'application'
+}
+
+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 {
+ jmeVersion = '3.7.0-stable'
+}
+
+dependencies {
+ implementation project(':blight-common')
+ implementation project(':blight-assets')
+
+ implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-desktop:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-lwjgl3:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-terrain:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-jbullet:${jmeVersion}"
+ implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
+ implementation 'com.google.code.gson:gson:2.11.0'
+}
+
+tasks.register('extractNatives', Copy) {
+ def nativeConf = configurations.runtimeClasspath.resolvedConfiguration
+ .resolvedArtifacts
+ .findAll { it.name.contains('natives') }
+ .collect { zipTree(it.file) }
+
+ from nativeConf
+ into "${buildDir}/natives"
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+}
+
+run {
+ dependsOn extractNatives
+ workingDir = rootDir // gemeinsames Arbeitsverzeichnis = Projekt-Root
+}
+
+jar {
+ manifest {
+ attributes 'Main-Class': application.mainClass
+ }
+}
diff --git a/blight-game/gradle/wrapper/gradle-wrapper.properties b/blight-game/gradle/wrapper/gradle-wrapper.properties
index b82aa23..a034286 100644
--- a/blight-game/gradle/wrapper/gradle-wrapper.properties
+++ b/blight-game/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/blight-game/gradlew b/blight-game/gradlew
index 97de990..30553ae 100755
--- a/blight-game/gradlew
+++ b/blight-game/gradlew
@@ -1,249 +1,249 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-#
-# Gradle start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh Gradle
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- if ! command -v java >/dev/null 2>&1
- then
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-# and any embedded shellness will be escaped.
-# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-# treated as '${Hostname}' itself on the command line.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/blight-game/gradlew.bat b/blight-game/gradlew.bat
index 16e26a1..66f1aa7 100644
--- a/blight-game/gradlew.bat
+++ b/blight-game/gradlew.bat
@@ -1,92 +1,92 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/blight-game/settings.gradle b/blight-game/settings.gradle
index f95b0de..b3c08c2 100644
--- a/blight-game/settings.gradle
+++ b/blight-game/settings.gradle
@@ -1 +1,8 @@
-rootProject.name = 'blight-game'
+rootProject.name = 'blight-game'
+
+// Sibling-Projekte einbinden.
+include 'blight-common'
+project(':blight-common').projectDir = new File(settingsDir, '../blight-common')
+
+include 'blight-assets'
+project(':blight-assets').projectDir = new File(settingsDir, '../blight-assets')
diff --git a/blight-game/src/main/java/de/blight/game/BlightApp.java b/blight-game/src/main/java/de/blight/game/BlightApp.java
index 204b91f..3a50592 100644
--- a/blight-game/src/main/java/de/blight/game/BlightApp.java
+++ b/blight-game/src/main/java/de/blight/game/BlightApp.java
@@ -1,92 +1,92 @@
-package de.blight.game;
-
-import com.jme3.app.SimpleApplication;
-import com.jme3.input.KeyInput;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.KeyTrigger;
-import com.jme3.system.AppSettings;
-import de.blight.game.config.*;
-import de.blight.game.scene.WorldScene;
-
-public class BlightApp extends SimpleApplication {
-
- private KeyBindings keyBindings;
- private GraphicsSettings graphicsSettings;
- private WorldScene worldScene;
- private ConfigScreen configScreen;
- private GraphicsScreen graphicsScreen;
- private PauseMenu pauseMenu;
-
- public static void main(String[] args) {
- BlightApp app = new BlightApp();
-
- GraphicsSettings gs = GraphicsStore.load();
- AppSettings settings = new AppSettings(true);
- settings.setTitle("Blight");
- settings.setResolution(gs.width, gs.height);
- settings.setFullscreen(gs.fullscreen);
- settings.setVSync(gs.vsync);
- settings.setSamples(gs.samples);
-
- app.setSettings(settings);
- app.setShowSettings(false);
- app.start();
- }
-
- @Override
- public void simpleInitApp() {
- flyCam.setEnabled(false);
- inputManager.deleteMapping(INPUT_MAPPING_EXIT);
-
- keyBindings = KeyBindingStore.load();
- graphicsSettings = GraphicsStore.load();
-
- worldScene = new WorldScene(keyBindings);
- stateManager.attach(worldScene);
-
- configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings));
- configScreen.setOnClose(() -> pauseMenu.setEnabled(true));
- stateManager.attach(configScreen);
- configScreen.setEnabled(false);
-
- graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true));
- stateManager.attach(graphicsScreen);
- graphicsScreen.setEnabled(false);
-
- pauseMenu = new PauseMenu(
- () -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); },
- () -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); }
- );
- stateManager.attach(pauseMenu);
- pauseMenu.setEnabled(false);
-
- inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE));
- inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
- if (!isPressed) return;
-
- if (graphicsScreen.isEnabled()) {
- // GraphicsScreen wird nur über seine eigenen Buttons geschlossen
- return;
- }
- if (configScreen.isEnabled()) {
- if (configScreen.isWaiting()) {
- configScreen.cancelWaiting();
- } else {
- configScreen.setEnabled(false);
- pauseMenu.setEnabled(true);
- }
- return;
- }
- if (pauseMenu.isEnabled()) {
- pauseMenu.setEnabled(false);
- worldScene.setPaused(false);
- return;
- }
- pauseMenu.setEnabled(true);
- worldScene.setPaused(true);
- }, "ToggleMenu");
- }
-
- @Override
- public void simpleUpdate(float tpf) {}
-}
+package de.blight.game;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.system.AppSettings;
+import de.blight.game.config.*;
+import de.blight.game.scene.WorldScene;
+
+public class BlightApp extends SimpleApplication {
+
+ private KeyBindings keyBindings;
+ private GraphicsSettings graphicsSettings;
+ private WorldScene worldScene;
+ private ConfigScreen configScreen;
+ private GraphicsScreen graphicsScreen;
+ private PauseMenu pauseMenu;
+
+ public static void main(String[] args) {
+ BlightApp app = new BlightApp();
+
+ GraphicsSettings gs = GraphicsStore.load();
+ AppSettings settings = new AppSettings(true);
+ settings.setTitle("Blight");
+ settings.setResolution(gs.width, gs.height);
+ settings.setFullscreen(gs.fullscreen);
+ settings.setVSync(gs.vsync);
+ settings.setSamples(gs.samples);
+
+ app.setSettings(settings);
+ app.setShowSettings(false);
+ app.start();
+ }
+
+ @Override
+ public void simpleInitApp() {
+ flyCam.setEnabled(false);
+ inputManager.deleteMapping(INPUT_MAPPING_EXIT);
+
+ keyBindings = KeyBindingStore.load();
+ graphicsSettings = GraphicsStore.load();
+
+ worldScene = new WorldScene(keyBindings);
+ stateManager.attach(worldScene);
+
+ configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings));
+ configScreen.setOnClose(() -> pauseMenu.setEnabled(true));
+ stateManager.attach(configScreen);
+ configScreen.setEnabled(false);
+
+ graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true));
+ stateManager.attach(graphicsScreen);
+ graphicsScreen.setEnabled(false);
+
+ pauseMenu = new PauseMenu(
+ () -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); },
+ () -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); }
+ );
+ stateManager.attach(pauseMenu);
+ pauseMenu.setEnabled(false);
+
+ inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE));
+ inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
+ if (!isPressed) return;
+
+ if (graphicsScreen.isEnabled()) {
+ // GraphicsScreen wird nur über seine eigenen Buttons geschlossen
+ return;
+ }
+ if (configScreen.isEnabled()) {
+ if (configScreen.isWaiting()) {
+ configScreen.cancelWaiting();
+ } else {
+ configScreen.setEnabled(false);
+ pauseMenu.setEnabled(true);
+ }
+ return;
+ }
+ if (pauseMenu.isEnabled()) {
+ pauseMenu.setEnabled(false);
+ worldScene.setPaused(false);
+ return;
+ }
+ pauseMenu.setEnabled(true);
+ worldScene.setPaused(true);
+ }, "ToggleMenu");
+ }
+
+ @Override
+ public void simpleUpdate(float tpf) {}
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java
index fd13c6a..db51a4c 100644
--- a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java
+++ b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java
@@ -1,309 +1,309 @@
-package de.blight.game.config;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import com.jme3.app.Application;
-import com.jme3.app.SimpleApplication;
-import com.jme3.app.state.BaseAppState;
-import com.jme3.font.BitmapFont;
-import com.jme3.font.BitmapText;
-import com.jme3.input.KeyInput;
-import com.jme3.input.MouseInput;
-import com.jme3.input.RawInputListener;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.MouseButtonTrigger;
-import com.jme3.input.event.JoyAxisEvent;
-import com.jme3.input.event.JoyButtonEvent;
-import com.jme3.input.event.KeyInputEvent;
-import com.jme3.input.event.MouseButtonEvent;
-import com.jme3.input.event.MouseMotionEvent;
-import com.jme3.input.event.TouchEvent;
-import com.jme3.material.Material;
-import com.jme3.material.RenderState;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Vector2f;
-import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Node;
-import com.jme3.scene.shape.Quad;
-
-/**
- * Overlay-AppState der die Tastenbelegungs-Maske anzeigt.
- *
- * ESC → Schließen (ohne Speichern)
- * Klick auf Row → wartet auf neue Taste
- * ESC während Warten → bricht nur die Zuweisung ab
- * Speichern → schreibt JSON, ruft onSave-Callback
- */
-public class ConfigScreen extends BaseAppState implements RawInputListener {
-
- // Farben
- private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
- private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
- private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
- private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f);
- private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
- private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
- private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
- private static final ColorRGBA COL_TEXT = ColorRGBA.White;
- private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
-
- // -----------------------------------------------------------------------
-
- private SimpleApplication app;
- private Node guiNode;
- private BitmapFont font;
-
- private KeyBindings liveBindings; // geteilt mit der ganzen App
- private KeyBindings editCopy; // wird beim Öffnen geklont
-
- private Runnable onSave; // Callback → PlayerInputControl.reloadBindings
- private Runnable onClose; // Callback → PauseMenu wiederherstellen
-
- private Node panel;
- private List rows = new ArrayList<>();
- private int waitingRow = -1; // -1 = keine Zuweisung aktiv
-
- // UI-Elemente für Buttons (Bounds in Screen-Koordinaten)
- private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
- private float cancelBtnX, cancelBtnY;
-
- // -----------------------------------------------------------------------
-
- private static class Row {
- String field;
- String label;
- BitmapText keyText;
- Geometry bg;
- float x, y, w, h; // Button-Bounds
- }
-
- // -----------------------------------------------------------------------
-
- public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
- this.liveBindings = liveBindings;
- this.onSave = onSave;
- }
-
- public boolean isWaiting() { return waitingRow >= 0; }
-
- public void setOnClose(Runnable onClose) { this.onClose = onClose; }
-
- public void cancelWaiting() {
- if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
- }
-
- // -----------------------------------------------------------------------
- // Lifecycle
- // -----------------------------------------------------------------------
-
- @Override
- protected void initialize(Application app) {
- this.app = (SimpleApplication) app;
- this.guiNode = this.app.getGuiNode();
- this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
- }
-
- @Override
- protected void onEnable() {
- editCopy = liveBindings.copy();
- waitingRow = -1;
- buildUI();
- app.getInputManager().setCursorVisible(true);
- app.getInputManager().addRawInputListener(this);
- app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
- app.getInputManager().addListener(clickListener, "_CfgClick");
- }
-
- @Override
- protected void onDisable() {
- if (panel != null) { guiNode.detachChild(panel); panel = null; }
- rows.clear();
- waitingRow = -1;
- app.getInputManager().removeRawInputListener(this);
- app.getInputManager().deleteMapping("_CfgClick");
- app.getInputManager().setCursorVisible(false);
- }
-
- @Override protected void cleanup(Application app) {}
-
- // -----------------------------------------------------------------------
- // UI aufbauen
- // -----------------------------------------------------------------------
-
- private void buildUI() {
- float sw = app.getCamera().getWidth();
- float sh = app.getCamera().getHeight();
-
- panel = new Node("cfg-panel");
-
- // Halbdurchsichtiger Overlay über dem Spiel
- addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
-
- float pw = 720, ph = 440;
- float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
- addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
-
- // Titel
- BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT);
- centerText(title, px, py + ph - 40, pw);
- panel.attachChild(title);
-
- BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
- centerText(hint, px, py + ph - 70, pw);
- panel.attachChild(hint);
-
- // Reihen
- float rowX = px + 30;
- float keyX = px + pw - 220;
- float rowW = 180;
- float rowH = 36;
- float startY = py + ph - 110;
- float stepY = 48;
-
- for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
- String[] entry = KeyBindings.ENTRIES[i];
- float ry = startY - i * stepY;
-
- BitmapText lbl = text(entry[1], 16, COL_TEXT);
- lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
- panel.attachChild(lbl);
-
- Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
-
- BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
- kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
- panel.attachChild(kt);
-
- Row row = new Row();
- row.field = entry[0];
- row.label = entry[1];
- row.keyText = kt;
- row.bg = bg;
- row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH;
- rows.add(row);
- }
-
- // Buttons
- float btnW = 160, btnH = 42;
- float btnY = py + 25;
- saveBtnX = px + pw / 2f - btnW - 15;
- saveBtnY = btnY;
- saveBtnW = btnW;
- saveBtnH = btnH;
- cancelBtnX = px + pw / 2f + 15;
- cancelBtnY = btnY;
-
- addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0);
- BitmapText saveLabel = text("Speichern", 16, COL_TEXT);
- centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
- panel.attachChild(saveLabel);
-
- addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0);
- BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT);
- centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
- panel.attachChild(cancelLabel);
-
- guiNode.attachChild(panel);
- }
-
- // -----------------------------------------------------------------------
- // Mausklick
- // -----------------------------------------------------------------------
-
- private final ActionListener clickListener = (name, isPressed, tpf) -> {
- if (!isPressed) return;
- Vector2f cursor = app.getInputManager().getCursorPosition();
-
- // Reihen prüfen
- for (int i = 0; i < rows.size(); i++) {
- Row r = rows.get(i);
- if (hits(cursor, r.x, r.y, r.w, r.h)) {
- waitingRow = i;
- r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
- r.keyText.setText("...");
- return;
- }
- }
-
- // Speichern
- if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
- liveBindings.copyFrom(editCopy);
- KeyBindingStore.save(liveBindings);
- if (onSave != null) onSave.run();
- setEnabled(false);
- if (onClose != null) onClose.run();
- return;
- }
-
- // Abbrechen
- if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
- setEnabled(false);
- if (onClose != null) onClose.run();
- }
- };
-
- // -----------------------------------------------------------------------
- // Tastendruck beim Warten auf Zuweisung (RawInputListener)
- // -----------------------------------------------------------------------
-
- @Override
- public void onKeyEvent(KeyInputEvent evt) {
- if (!evt.isPressed() || waitingRow < 0) return;
- if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen
-
- Row r = rows.get(waitingRow);
- editCopy.set(r.field, evt.getKeyCode());
- r.keyText.setText(KeyNames.of(evt.getKeyCode()));
- resetRowColor(waitingRow);
- waitingRow = -1;
- }
-
- private void resetRowColor(int idx) {
- rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
- }
-
- // RawInputListener-Pflichtmethoden
- @Override public void beginInput() {}
- @Override public void endInput() {}
- @Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
- @Override public void onMouseButtonEvent(MouseButtonEvent evt) {}
- @Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
- @Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
- @Override public void onTouchEvent(TouchEvent evt) {}
-
- // -----------------------------------------------------------------------
- // Hilfsmethoden
- // -----------------------------------------------------------------------
-
- private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
- Geometry geo = new Geometry("q", new Quad(w, h));
- Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", color.clone());
- if (color.a < 1f) {
- mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- geo.setQueueBucket(RenderQueue.Bucket.Transparent);
- }
- geo.setMaterial(mat);
- geo.setLocalTranslation(x, y, z);
- parent.attachChild(geo);
- return geo;
- }
-
- private BitmapText text(String content, int size, ColorRGBA color) {
- BitmapText t = new BitmapText(font, false);
- t.setSize(size);
- t.setColor(color);
- t.setText(content);
- return t;
- }
-
- private void centerText(BitmapText t, float x, float y, float width) {
- t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
- }
-
- private boolean hits(Vector2f p, float x, float y, float w, float h) {
- return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
- }
-}
+package de.blight.game.config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.MouseInput;
+import com.jme3.input.RawInputListener;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.input.event.JoyAxisEvent;
+import com.jme3.input.event.JoyButtonEvent;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Quad;
+
+/**
+ * Overlay-AppState der die Tastenbelegungs-Maske anzeigt.
+ *
+ * ESC → Schließen (ohne Speichern)
+ * Klick auf Row → wartet auf neue Taste
+ * ESC während Warten → bricht nur die Zuweisung ab
+ * Speichern → schreibt JSON, ruft onSave-Callback
+ */
+public class ConfigScreen extends BaseAppState implements RawInputListener {
+
+ // Farben
+ private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
+ private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
+ private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
+ private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f);
+ private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
+ private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
+ private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
+ private static final ColorRGBA COL_TEXT = ColorRGBA.White;
+ private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
+
+ // -----------------------------------------------------------------------
+
+ private SimpleApplication app;
+ private Node guiNode;
+ private BitmapFont font;
+
+ private KeyBindings liveBindings; // geteilt mit der ganzen App
+ private KeyBindings editCopy; // wird beim Öffnen geklont
+
+ private Runnable onSave; // Callback → PlayerInputControl.reloadBindings
+ private Runnable onClose; // Callback → PauseMenu wiederherstellen
+
+ private Node panel;
+ private List rows = new ArrayList<>();
+ private int waitingRow = -1; // -1 = keine Zuweisung aktiv
+
+ // UI-Elemente für Buttons (Bounds in Screen-Koordinaten)
+ private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
+ private float cancelBtnX, cancelBtnY;
+
+ // -----------------------------------------------------------------------
+
+ private static class Row {
+ String field;
+ String label;
+ BitmapText keyText;
+ Geometry bg;
+ float x, y, w, h; // Button-Bounds
+ }
+
+ // -----------------------------------------------------------------------
+
+ public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
+ this.liveBindings = liveBindings;
+ this.onSave = onSave;
+ }
+
+ public boolean isWaiting() { return waitingRow >= 0; }
+
+ public void setOnClose(Runnable onClose) { this.onClose = onClose; }
+
+ public void cancelWaiting() {
+ if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
+ }
+
+ // -----------------------------------------------------------------------
+ // Lifecycle
+ // -----------------------------------------------------------------------
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.guiNode = this.app.getGuiNode();
+ this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
+ }
+
+ @Override
+ protected void onEnable() {
+ editCopy = liveBindings.copy();
+ waitingRow = -1;
+ buildUI();
+ app.getInputManager().setCursorVisible(true);
+ app.getInputManager().addRawInputListener(this);
+ app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+ app.getInputManager().addListener(clickListener, "_CfgClick");
+ }
+
+ @Override
+ protected void onDisable() {
+ if (panel != null) { guiNode.detachChild(panel); panel = null; }
+ rows.clear();
+ waitingRow = -1;
+ app.getInputManager().removeRawInputListener(this);
+ app.getInputManager().deleteMapping("_CfgClick");
+ app.getInputManager().setCursorVisible(false);
+ }
+
+ @Override protected void cleanup(Application app) {}
+
+ // -----------------------------------------------------------------------
+ // UI aufbauen
+ // -----------------------------------------------------------------------
+
+ private void buildUI() {
+ float sw = app.getCamera().getWidth();
+ float sh = app.getCamera().getHeight();
+
+ panel = new Node("cfg-panel");
+
+ // Halbdurchsichtiger Overlay über dem Spiel
+ addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
+
+ float pw = 720, ph = 440;
+ float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
+ addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
+
+ // Titel
+ BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT);
+ centerText(title, px, py + ph - 40, pw);
+ panel.attachChild(title);
+
+ BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
+ centerText(hint, px, py + ph - 70, pw);
+ panel.attachChild(hint);
+
+ // Reihen
+ float rowX = px + 30;
+ float keyX = px + pw - 220;
+ float rowW = 180;
+ float rowH = 36;
+ float startY = py + ph - 110;
+ float stepY = 48;
+
+ for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
+ String[] entry = KeyBindings.ENTRIES[i];
+ float ry = startY - i * stepY;
+
+ BitmapText lbl = text(entry[1], 16, COL_TEXT);
+ lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
+ panel.attachChild(lbl);
+
+ Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
+
+ BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
+ kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
+ panel.attachChild(kt);
+
+ Row row = new Row();
+ row.field = entry[0];
+ row.label = entry[1];
+ row.keyText = kt;
+ row.bg = bg;
+ row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH;
+ rows.add(row);
+ }
+
+ // Buttons
+ float btnW = 160, btnH = 42;
+ float btnY = py + 25;
+ saveBtnX = px + pw / 2f - btnW - 15;
+ saveBtnY = btnY;
+ saveBtnW = btnW;
+ saveBtnH = btnH;
+ cancelBtnX = px + pw / 2f + 15;
+ cancelBtnY = btnY;
+
+ addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0);
+ BitmapText saveLabel = text("Speichern", 16, COL_TEXT);
+ centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
+ panel.attachChild(saveLabel);
+
+ addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0);
+ BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT);
+ centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
+ panel.attachChild(cancelLabel);
+
+ guiNode.attachChild(panel);
+ }
+
+ // -----------------------------------------------------------------------
+ // Mausklick
+ // -----------------------------------------------------------------------
+
+ private final ActionListener clickListener = (name, isPressed, tpf) -> {
+ if (!isPressed) return;
+ Vector2f cursor = app.getInputManager().getCursorPosition();
+
+ // Reihen prüfen
+ for (int i = 0; i < rows.size(); i++) {
+ Row r = rows.get(i);
+ if (hits(cursor, r.x, r.y, r.w, r.h)) {
+ waitingRow = i;
+ r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
+ r.keyText.setText("...");
+ return;
+ }
+ }
+
+ // Speichern
+ if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
+ liveBindings.copyFrom(editCopy);
+ KeyBindingStore.save(liveBindings);
+ if (onSave != null) onSave.run();
+ setEnabled(false);
+ if (onClose != null) onClose.run();
+ return;
+ }
+
+ // Abbrechen
+ if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
+ setEnabled(false);
+ if (onClose != null) onClose.run();
+ }
+ };
+
+ // -----------------------------------------------------------------------
+ // Tastendruck beim Warten auf Zuweisung (RawInputListener)
+ // -----------------------------------------------------------------------
+
+ @Override
+ public void onKeyEvent(KeyInputEvent evt) {
+ if (!evt.isPressed() || waitingRow < 0) return;
+ if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen
+
+ Row r = rows.get(waitingRow);
+ editCopy.set(r.field, evt.getKeyCode());
+ r.keyText.setText(KeyNames.of(evt.getKeyCode()));
+ resetRowColor(waitingRow);
+ waitingRow = -1;
+ }
+
+ private void resetRowColor(int idx) {
+ rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
+ }
+
+ // RawInputListener-Pflichtmethoden
+ @Override public void beginInput() {}
+ @Override public void endInput() {}
+ @Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
+ @Override public void onMouseButtonEvent(MouseButtonEvent evt) {}
+ @Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
+ @Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
+ @Override public void onTouchEvent(TouchEvent evt) {}
+
+ // -----------------------------------------------------------------------
+ // Hilfsmethoden
+ // -----------------------------------------------------------------------
+
+ private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
+ Geometry geo = new Geometry("q", new Quad(w, h));
+ Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color.clone());
+ if (color.a < 1f) {
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ geo.setQueueBucket(RenderQueue.Bucket.Transparent);
+ }
+ geo.setMaterial(mat);
+ geo.setLocalTranslation(x, y, z);
+ parent.attachChild(geo);
+ return geo;
+ }
+
+ private BitmapText text(String content, int size, ColorRGBA color) {
+ BitmapText t = new BitmapText(font, false);
+ t.setSize(size);
+ t.setColor(color);
+ t.setText(content);
+ return t;
+ }
+
+ private void centerText(BitmapText t, float x, float y, float width) {
+ t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
+ }
+
+ private boolean hits(Vector2f p, float x, float y, float w, float h) {
+ return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java
index fb884a5..917bed7 100644
--- a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java
+++ b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java
@@ -1,290 +1,290 @@
-package de.blight.game.config;
-
-import com.jme3.app.Application;
-import com.jme3.app.SimpleApplication;
-import com.jme3.app.state.BaseAppState;
-import com.jme3.font.BitmapFont;
-import com.jme3.font.BitmapText;
-import com.jme3.input.MouseInput;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.MouseButtonTrigger;
-import com.jme3.material.Material;
-import com.jme3.material.RenderState;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Vector2f;
-import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Node;
-import com.jme3.scene.shape.Quad;
-import com.jme3.system.AppSettings;
-
-public class GraphicsScreen extends BaseAppState {
-
- private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
- private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
- private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
- private static final ColorRGBA COL_ARROW = new ColorRGBA(0.28f, 0.28f, 0.44f, 1.00f);
- private static final ColorRGBA COL_BTN_OK = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
- private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
- private static final ColorRGBA COL_TEXT = ColorRGBA.White;
- private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
-
- private static final int[][] RESOLUTIONS = {
- {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
- };
- private static final int[] SAMPLES = {0, 2, 4, 8};
-
- private static final int ROW_RES = 0;
- private static final int ROW_FULL = 1;
- private static final int ROW_VSYNC = 2;
- private static final int ROW_AA = 3;
-
- private SimpleApplication app;
- private Node guiNode;
- private BitmapFont font;
- private Node panel;
-
- private final GraphicsSettings live;
- private GraphicsSettings edit;
- private final Runnable onClose;
-
- private int resIdx;
- private int samplesIdx;
-
- // Per-row layout (indexed by ROW_*)
- private final float[] cellX = new float[4];
- private final float[] cellY = new float[4];
- private final float[] cellW = new float[4];
- private final float cellH = 36;
- private final float arrW = 30;
- private final float[] leftX = new float[4];
- private final float[] rightX = new float[4];
- private final BitmapText[] valTexts = new BitmapText[4];
-
- private float okX, okY, okW, okH;
- private float cancelX, cancelY;
-
- public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
- this.live = live;
- this.onClose = onClose;
- }
-
- @Override
- protected void initialize(Application app) {
- this.app = (SimpleApplication) app;
- this.guiNode = this.app.getGuiNode();
- this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
- }
-
- @Override
- protected void onEnable() {
- edit = new GraphicsSettings();
- edit.width = live.width; edit.height = live.height;
- edit.fullscreen = live.fullscreen;
- edit.vsync = live.vsync;
- edit.samples = live.samples;
-
- resIdx = 0;
- for (int i = 0; i < RESOLUTIONS.length; i++) {
- if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) {
- resIdx = i; break;
- }
- }
- samplesIdx = 0;
- for (int i = 0; i < SAMPLES.length; i++) {
- if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; }
- }
-
- buildUI();
- app.getInputManager().setCursorVisible(true);
- app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
- app.getInputManager().addListener(clickListener, "_GfxClick");
- }
-
- @Override
- protected void onDisable() {
- if (panel != null) { guiNode.detachChild(panel); panel = null; }
- app.getInputManager().deleteMapping("_GfxClick");
- app.getInputManager().setCursorVisible(false);
- }
-
- @Override protected void cleanup(Application app) {}
-
- private void buildUI() {
- float sw = app.getCamera().getWidth();
- float sh = app.getCamera().getHeight();
- panel = new Node("gfx-panel");
- addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
-
- float pw = 640, ph = 400;
- float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
- addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
-
- BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT);
- centerText(title, px, py + ph - 42, pw);
- panel.attachChild(title);
-
- String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"};
- float lblX = px + 30;
- float vx = px + pw - 270;
- float vw = 190;
- float startY = py + ph - 100;
- float step = 60;
-
- for (int i = 0; i < 4; i++) {
- float ry = startY - i * step;
-
- BitmapText lbl = txt(labels[i], 16, COL_TEXT);
- lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
- panel.attachChild(lbl);
-
- // Left arrow
- addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0);
- BitmapText lt = txt("<", 16, COL_TEXT);
- lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
- panel.attachChild(lt);
-
- // Value cell
- addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0);
-
- // Right arrow
- addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0);
- BitmapText rt = txt(">", 16, COL_TEXT);
- rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
- panel.attachChild(rt);
-
- BitmapText vt = txt("", 16, COL_TEXT_VAL);
- panel.attachChild(vt);
- valTexts[i] = vt;
-
- cellX[i] = vx; cellY[i] = ry; cellW[i] = vw;
- leftX[i] = vx - arrW - 6;
- rightX[i] = vx + vw + 6;
- }
-
- for (int i = 0; i < 4; i++) refreshText(i);
-
- float bw = 160, bh = 42;
- okW = bw; okH = bh;
- okX = px + pw / 2f - bw - 10;
- okY = py + 22;
- cancelX = px + pw / 2f + 10;
- cancelY = py + 22;
-
- addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0);
- BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT);
- centerText(okLbl, okX, okY + bh - 10, bw);
- panel.attachChild(okLbl);
-
- addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0);
- BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT);
- centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
- panel.attachChild(cancelLbl);
-
- guiNode.attachChild(panel);
- }
-
- private void refreshText(int row) {
- String val = switch (row) {
- case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
- case ROW_FULL -> edit.fullscreen ? "An" : "Aus";
- case ROW_VSYNC -> edit.vsync ? "An" : "Aus";
- case ROW_AA -> SAMPLES[samplesIdx] == 0 ? "Aus" : SAMPLES[samplesIdx] + "x MSAA";
- default -> "";
- };
- BitmapText vt = valTexts[row];
- vt.setText(val);
- vt.setLocalTranslation(
- cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
- cellY[row] + cellH - 8,
- 1
- );
- }
-
- private final ActionListener clickListener = (name, isPressed, tpf) -> {
- if (!isPressed) return;
- Vector2f c = app.getInputManager().getCursorPosition();
-
- for (int i = 0; i < 4; i++) {
- if (hits(c, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; }
- if (hits(c, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; }
- }
- if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; }
- if (hits(c, cancelX, cancelY, okW, okH)) { close(); }
- };
-
- private void cycleRow(int row, int dir) {
- switch (row) {
- case ROW_RES:
- resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length;
- edit.width = RESOLUTIONS[resIdx][0];
- edit.height = RESOLUTIONS[resIdx][1];
- break;
- case ROW_FULL:
- edit.fullscreen = !edit.fullscreen;
- break;
- case ROW_VSYNC:
- edit.vsync = !edit.vsync;
- break;
- case ROW_AA:
- samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
- edit.samples = SAMPLES[samplesIdx];
- break;
- }
- refreshText(row);
- }
-
- private void applyAndSave() {
- live.width = edit.width; live.height = edit.height;
- live.fullscreen = edit.fullscreen;
- live.vsync = edit.vsync;
- live.samples = edit.samples;
-
- GraphicsStore.save(live);
-
- AppSettings s = app.getContext().getSettings();
- s.setResolution(live.width, live.height);
- s.setFullscreen(live.fullscreen);
- s.setVSync(live.vsync);
- s.setSamples(live.samples);
- app.setSettings(s);
-
- close();
- app.restart();
- }
-
- private void close() {
- setEnabled(false);
- if (onClose != null) onClose.run();
- }
-
- // -----------------------------------------------------------------------
-
- private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
- Geometry geo = new Geometry("q", new Quad(w, h));
- Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", color.clone());
- if (color.a < 1f) {
- mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- geo.setQueueBucket(RenderQueue.Bucket.Transparent);
- }
- geo.setMaterial(mat);
- geo.setLocalTranslation(x, y, z);
- parent.attachChild(geo);
- return geo;
- }
-
- private BitmapText txt(String s, int size, ColorRGBA color) {
- BitmapText t = new BitmapText(font, false);
- t.setSize(size); t.setColor(color); t.setText(s);
- return t;
- }
-
- private void centerText(BitmapText t, float x, float y, float width) {
- t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
- }
-
- private boolean hits(Vector2f p, float x, float y, float w, float h) {
- return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
- }
-}
+package de.blight.game.config;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Quad;
+import com.jme3.system.AppSettings;
+
+public class GraphicsScreen extends BaseAppState {
+
+ private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
+ private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
+ private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
+ private static final ColorRGBA COL_ARROW = new ColorRGBA(0.28f, 0.28f, 0.44f, 1.00f);
+ private static final ColorRGBA COL_BTN_OK = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
+ private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
+ private static final ColorRGBA COL_TEXT = ColorRGBA.White;
+ private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
+
+ private static final int[][] RESOLUTIONS = {
+ {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
+ };
+ private static final int[] SAMPLES = {0, 2, 4, 8};
+
+ private static final int ROW_RES = 0;
+ private static final int ROW_FULL = 1;
+ private static final int ROW_VSYNC = 2;
+ private static final int ROW_AA = 3;
+
+ private SimpleApplication app;
+ private Node guiNode;
+ private BitmapFont font;
+ private Node panel;
+
+ private final GraphicsSettings live;
+ private GraphicsSettings edit;
+ private final Runnable onClose;
+
+ private int resIdx;
+ private int samplesIdx;
+
+ // Per-row layout (indexed by ROW_*)
+ private final float[] cellX = new float[4];
+ private final float[] cellY = new float[4];
+ private final float[] cellW = new float[4];
+ private final float cellH = 36;
+ private final float arrW = 30;
+ private final float[] leftX = new float[4];
+ private final float[] rightX = new float[4];
+ private final BitmapText[] valTexts = new BitmapText[4];
+
+ private float okX, okY, okW, okH;
+ private float cancelX, cancelY;
+
+ public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
+ this.live = live;
+ this.onClose = onClose;
+ }
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.guiNode = this.app.getGuiNode();
+ this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
+ }
+
+ @Override
+ protected void onEnable() {
+ edit = new GraphicsSettings();
+ edit.width = live.width; edit.height = live.height;
+ edit.fullscreen = live.fullscreen;
+ edit.vsync = live.vsync;
+ edit.samples = live.samples;
+
+ resIdx = 0;
+ for (int i = 0; i < RESOLUTIONS.length; i++) {
+ if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) {
+ resIdx = i; break;
+ }
+ }
+ samplesIdx = 0;
+ for (int i = 0; i < SAMPLES.length; i++) {
+ if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; }
+ }
+
+ buildUI();
+ app.getInputManager().setCursorVisible(true);
+ app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+ app.getInputManager().addListener(clickListener, "_GfxClick");
+ }
+
+ @Override
+ protected void onDisable() {
+ if (panel != null) { guiNode.detachChild(panel); panel = null; }
+ app.getInputManager().deleteMapping("_GfxClick");
+ app.getInputManager().setCursorVisible(false);
+ }
+
+ @Override protected void cleanup(Application app) {}
+
+ private void buildUI() {
+ float sw = app.getCamera().getWidth();
+ float sh = app.getCamera().getHeight();
+ panel = new Node("gfx-panel");
+ addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
+
+ float pw = 640, ph = 400;
+ float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
+ addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
+
+ BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT);
+ centerText(title, px, py + ph - 42, pw);
+ panel.attachChild(title);
+
+ String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"};
+ float lblX = px + 30;
+ float vx = px + pw - 270;
+ float vw = 190;
+ float startY = py + ph - 100;
+ float step = 60;
+
+ for (int i = 0; i < 4; i++) {
+ float ry = startY - i * step;
+
+ BitmapText lbl = txt(labels[i], 16, COL_TEXT);
+ lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
+ panel.attachChild(lbl);
+
+ // Left arrow
+ addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0);
+ BitmapText lt = txt("<", 16, COL_TEXT);
+ lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
+ panel.attachChild(lt);
+
+ // Value cell
+ addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0);
+
+ // Right arrow
+ addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0);
+ BitmapText rt = txt(">", 16, COL_TEXT);
+ rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
+ panel.attachChild(rt);
+
+ BitmapText vt = txt("", 16, COL_TEXT_VAL);
+ panel.attachChild(vt);
+ valTexts[i] = vt;
+
+ cellX[i] = vx; cellY[i] = ry; cellW[i] = vw;
+ leftX[i] = vx - arrW - 6;
+ rightX[i] = vx + vw + 6;
+ }
+
+ for (int i = 0; i < 4; i++) refreshText(i);
+
+ float bw = 160, bh = 42;
+ okW = bw; okH = bh;
+ okX = px + pw / 2f - bw - 10;
+ okY = py + 22;
+ cancelX = px + pw / 2f + 10;
+ cancelY = py + 22;
+
+ addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0);
+ BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT);
+ centerText(okLbl, okX, okY + bh - 10, bw);
+ panel.attachChild(okLbl);
+
+ addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0);
+ BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT);
+ centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
+ panel.attachChild(cancelLbl);
+
+ guiNode.attachChild(panel);
+ }
+
+ private void refreshText(int row) {
+ String val = switch (row) {
+ case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
+ case ROW_FULL -> edit.fullscreen ? "An" : "Aus";
+ case ROW_VSYNC -> edit.vsync ? "An" : "Aus";
+ case ROW_AA -> SAMPLES[samplesIdx] == 0 ? "Aus" : SAMPLES[samplesIdx] + "x MSAA";
+ default -> "";
+ };
+ BitmapText vt = valTexts[row];
+ vt.setText(val);
+ vt.setLocalTranslation(
+ cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
+ cellY[row] + cellH - 8,
+ 1
+ );
+ }
+
+ private final ActionListener clickListener = (name, isPressed, tpf) -> {
+ if (!isPressed) return;
+ Vector2f c = app.getInputManager().getCursorPosition();
+
+ for (int i = 0; i < 4; i++) {
+ if (hits(c, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; }
+ if (hits(c, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; }
+ }
+ if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; }
+ if (hits(c, cancelX, cancelY, okW, okH)) { close(); }
+ };
+
+ private void cycleRow(int row, int dir) {
+ switch (row) {
+ case ROW_RES:
+ resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length;
+ edit.width = RESOLUTIONS[resIdx][0];
+ edit.height = RESOLUTIONS[resIdx][1];
+ break;
+ case ROW_FULL:
+ edit.fullscreen = !edit.fullscreen;
+ break;
+ case ROW_VSYNC:
+ edit.vsync = !edit.vsync;
+ break;
+ case ROW_AA:
+ samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
+ edit.samples = SAMPLES[samplesIdx];
+ break;
+ }
+ refreshText(row);
+ }
+
+ private void applyAndSave() {
+ live.width = edit.width; live.height = edit.height;
+ live.fullscreen = edit.fullscreen;
+ live.vsync = edit.vsync;
+ live.samples = edit.samples;
+
+ GraphicsStore.save(live);
+
+ AppSettings s = app.getContext().getSettings();
+ s.setResolution(live.width, live.height);
+ s.setFullscreen(live.fullscreen);
+ s.setVSync(live.vsync);
+ s.setSamples(live.samples);
+ app.setSettings(s);
+
+ close();
+ app.restart();
+ }
+
+ private void close() {
+ setEnabled(false);
+ if (onClose != null) onClose.run();
+ }
+
+ // -----------------------------------------------------------------------
+
+ private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
+ Geometry geo = new Geometry("q", new Quad(w, h));
+ Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color.clone());
+ if (color.a < 1f) {
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ geo.setQueueBucket(RenderQueue.Bucket.Transparent);
+ }
+ geo.setMaterial(mat);
+ geo.setLocalTranslation(x, y, z);
+ parent.attachChild(geo);
+ return geo;
+ }
+
+ private BitmapText txt(String s, int size, ColorRGBA color) {
+ BitmapText t = new BitmapText(font, false);
+ t.setSize(size); t.setColor(color); t.setText(s);
+ return t;
+ }
+
+ private void centerText(BitmapText t, float x, float y, float width) {
+ t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
+ }
+
+ private boolean hits(Vector2f p, float x, float y, float w, float h) {
+ return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java
index 9f72757..fbc9ea9 100644
--- a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java
+++ b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java
@@ -1,9 +1,9 @@
-package de.blight.game.config;
-
-public class GraphicsSettings {
- public int width = 1280;
- public int height = 720;
- public boolean fullscreen = false;
- public boolean vsync = false;
- public int samples = 4;
-}
+package de.blight.game.config;
+
+public class GraphicsSettings {
+ public int width = 1280;
+ public int height = 720;
+ public boolean fullscreen = false;
+ public boolean vsync = false;
+ public int samples = 4;
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java
index 9f24c44..f1377bc 100644
--- a/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java
+++ b/blight-game/src/main/java/de/blight/game/config/GraphicsStore.java
@@ -1,35 +1,35 @@
-package de.blight.game.config;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import java.io.*;
-import java.nio.file.*;
-
-public class GraphicsStore {
-
- private static final Path FILE = Paths.get("config", "graphics.json");
- private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
-
- public static GraphicsSettings load() {
- if (Files.exists(FILE)) {
- try (Reader r = Files.newBufferedReader(FILE)) {
- return GSON.fromJson(r, GraphicsSettings.class);
- } catch (IOException e) {
- System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage());
- }
- }
- return new GraphicsSettings();
- }
-
- public static void save(GraphicsSettings gs) {
- try {
- Files.createDirectories(FILE.getParent());
- try (Writer w = Files.newBufferedWriter(FILE)) {
- GSON.toJson(gs, w);
- }
- } catch (IOException e) {
- System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage());
- }
- }
-}
+package de.blight.game.config;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.io.*;
+import java.nio.file.*;
+
+public class GraphicsStore {
+
+ private static final Path FILE = Paths.get("config", "graphics.json");
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+
+ public static GraphicsSettings load() {
+ if (Files.exists(FILE)) {
+ try (Reader r = Files.newBufferedReader(FILE)) {
+ return GSON.fromJson(r, GraphicsSettings.class);
+ } catch (IOException e) {
+ System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage());
+ }
+ }
+ return new GraphicsSettings();
+ }
+
+ public static void save(GraphicsSettings gs) {
+ try {
+ Files.createDirectories(FILE.getParent());
+ try (Writer w = Files.newBufferedWriter(FILE)) {
+ GSON.toJson(gs, w);
+ }
+ } catch (IOException e) {
+ System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage());
+ }
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java
index a15e8bf..23cfbdb 100644
--- a/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java
+++ b/blight-game/src/main/java/de/blight/game/config/KeyBindingStore.java
@@ -1,35 +1,35 @@
-package de.blight.game.config;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-import java.io.*;
-import java.nio.file.*;
-
-public class KeyBindingStore {
-
- private static final Path FILE = Paths.get("config", "keybindings.json");
- private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
-
- public static KeyBindings load() {
- if (Files.exists(FILE)) {
- try (Reader r = Files.newBufferedReader(FILE)) {
- return GSON.fromJson(r, KeyBindings.class);
- } catch (IOException e) {
- System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage());
- }
- }
- return new KeyBindings();
- }
-
- public static void save(KeyBindings kb) {
- try {
- Files.createDirectories(FILE.getParent());
- try (Writer w = Files.newBufferedWriter(FILE)) {
- GSON.toJson(kb, w);
- }
- } catch (IOException e) {
- System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage());
- }
- }
-}
+package de.blight.game.config;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.io.*;
+import java.nio.file.*;
+
+public class KeyBindingStore {
+
+ private static final Path FILE = Paths.get("config", "keybindings.json");
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+
+ public static KeyBindings load() {
+ if (Files.exists(FILE)) {
+ try (Reader r = Files.newBufferedReader(FILE)) {
+ return GSON.fromJson(r, KeyBindings.class);
+ } catch (IOException e) {
+ System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage());
+ }
+ }
+ return new KeyBindings();
+ }
+
+ public static void save(KeyBindings kb) {
+ try {
+ Files.createDirectories(FILE.getParent());
+ try (Writer w = Files.newBufferedWriter(FILE)) {
+ GSON.toJson(kb, w);
+ }
+ } catch (IOException e) {
+ System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage());
+ }
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java
index 797a201..a64eb2e 100644
--- a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java
+++ b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java
@@ -1,44 +1,44 @@
-package de.blight.game.config;
-
-import com.jme3.input.KeyInput;
-
-/** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */
-public class KeyBindings {
-
- public int forward = KeyInput.KEY_W;
- public int backward = KeyInput.KEY_S;
- public int left = KeyInput.KEY_A;
- public int right = KeyInput.KEY_D;
- public int jump = KeyInput.KEY_SPACE;
- public int sprint = KeyInput.KEY_LSHIFT;
-
- /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
- public static final String[][] ENTRIES = {
- {"forward", "Vorwärts"},
- {"backward", "Rückwärts"},
- {"left", "Links"},
- {"right", "Rechts"},
- {"jump", "Springen"},
- {"sprint", "Rennen"},
- };
-
- public int get(String fieldName) {
- try { return (int) KeyBindings.class.getField(fieldName).get(this); }
- catch (Exception e) { return 0; }
- }
-
- public void set(String fieldName, int keyCode) {
- try { KeyBindings.class.getField(fieldName).setInt(this, keyCode); }
- catch (Exception ignored) {}
- }
-
- public KeyBindings copy() {
- KeyBindings c = new KeyBindings();
- for (String[] e : ENTRIES) c.set(e[0], get(e[0]));
- return c;
- }
-
- public void copyFrom(KeyBindings src) {
- for (String[] e : ENTRIES) set(e[0], src.get(e[0]));
- }
-}
+package de.blight.game.config;
+
+import com.jme3.input.KeyInput;
+
+/** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */
+public class KeyBindings {
+
+ public int forward = KeyInput.KEY_W;
+ public int backward = KeyInput.KEY_S;
+ public int left = KeyInput.KEY_A;
+ public int right = KeyInput.KEY_D;
+ public int jump = KeyInput.KEY_SPACE;
+ public int sprint = KeyInput.KEY_LSHIFT;
+
+ /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
+ public static final String[][] ENTRIES = {
+ {"forward", "Vorwärts"},
+ {"backward", "Rückwärts"},
+ {"left", "Links"},
+ {"right", "Rechts"},
+ {"jump", "Springen"},
+ {"sprint", "Rennen"},
+ };
+
+ public int get(String fieldName) {
+ try { return (int) KeyBindings.class.getField(fieldName).get(this); }
+ catch (Exception e) { return 0; }
+ }
+
+ public void set(String fieldName, int keyCode) {
+ try { KeyBindings.class.getField(fieldName).setInt(this, keyCode); }
+ catch (Exception ignored) {}
+ }
+
+ public KeyBindings copy() {
+ KeyBindings c = new KeyBindings();
+ for (String[] e : ENTRIES) c.set(e[0], get(e[0]));
+ return c;
+ }
+
+ public void copyFrom(KeyBindings src) {
+ for (String[] e : ENTRIES) set(e[0], src.get(e[0]));
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/KeyNames.java b/blight-game/src/main/java/de/blight/game/config/KeyNames.java
index ce48361..ab91299 100644
--- a/blight-game/src/main/java/de/blight/game/config/KeyNames.java
+++ b/blight-game/src/main/java/de/blight/game/config/KeyNames.java
@@ -1,51 +1,51 @@
-package de.blight.game.config;
-
-import com.jme3.input.KeyInput;
-
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Bildet KeyInput-Codes auf lesbare Namen ab (via Reflection auf KeyInput-Konstanten). */
-public final class KeyNames {
-
- private static final Map NAMES = new HashMap<>();
-
- static {
- for (Field f : KeyInput.class.getFields()) {
- if (f.getName().startsWith("KEY_") && f.getType() == int.class) {
- try {
- NAMES.put(f.getInt(null), pretty(f.getName().substring(4)));
- } catch (IllegalAccessException ignored) {}
- }
- }
- }
-
- public static String of(int keyCode) {
- return NAMES.getOrDefault(keyCode, "Key#" + keyCode);
- }
-
- private static String pretty(String raw) {
- // "LSHIFT" → "L-Shift", "SPACE" → "Space", "W" → "W"
- return switch (raw) {
- case "SPACE" -> "Space";
- case "LSHIFT" -> "L-Shift";
- case "RSHIFT" -> "R-Shift";
- case "LCONTROL"-> "L-Ctrl";
- case "RCONTROL"-> "R-Ctrl";
- case "LMENU" -> "L-Alt";
- case "RMENU" -> "R-Alt";
- case "RETURN" -> "Enter";
- case "BACK" -> "Backspace";
- case "UP" -> "Pfeil-Hoch";
- case "DOWN" -> "Pfeil-Runter";
- case "LEFT" -> "Pfeil-Links";
- case "RIGHT" -> "Pfeil-Rechts";
- default -> raw.length() == 1 ? raw : capitalize(raw);
- };
- }
-
- private static String capitalize(String s) {
- return s.isEmpty() ? s : s.charAt(0) + s.substring(1).toLowerCase();
- }
-}
+package de.blight.game.config;
+
+import com.jme3.input.KeyInput;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Bildet KeyInput-Codes auf lesbare Namen ab (via Reflection auf KeyInput-Konstanten). */
+public final class KeyNames {
+
+ private static final Map NAMES = new HashMap<>();
+
+ static {
+ for (Field f : KeyInput.class.getFields()) {
+ if (f.getName().startsWith("KEY_") && f.getType() == int.class) {
+ try {
+ NAMES.put(f.getInt(null), pretty(f.getName().substring(4)));
+ } catch (IllegalAccessException ignored) {}
+ }
+ }
+ }
+
+ public static String of(int keyCode) {
+ return NAMES.getOrDefault(keyCode, "Key#" + keyCode);
+ }
+
+ private static String pretty(String raw) {
+ // "LSHIFT" → "L-Shift", "SPACE" → "Space", "W" → "W"
+ return switch (raw) {
+ case "SPACE" -> "Space";
+ case "LSHIFT" -> "L-Shift";
+ case "RSHIFT" -> "R-Shift";
+ case "LCONTROL"-> "L-Ctrl";
+ case "RCONTROL"-> "R-Ctrl";
+ case "LMENU" -> "L-Alt";
+ case "RMENU" -> "R-Alt";
+ case "RETURN" -> "Enter";
+ case "BACK" -> "Backspace";
+ case "UP" -> "Pfeil-Hoch";
+ case "DOWN" -> "Pfeil-Runter";
+ case "LEFT" -> "Pfeil-Links";
+ case "RIGHT" -> "Pfeil-Rechts";
+ default -> raw.length() == 1 ? raw : capitalize(raw);
+ };
+ }
+
+ private static String capitalize(String s) {
+ return s.isEmpty() ? s : s.charAt(0) + s.substring(1).toLowerCase();
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java
index d5bce24..6b26941 100644
--- a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java
+++ b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java
@@ -1,168 +1,168 @@
-package de.blight.game.config;
-
-import com.jme3.app.Application;
-import com.jme3.app.SimpleApplication;
-import com.jme3.app.state.BaseAppState;
-import com.jme3.font.BitmapFont;
-import com.jme3.font.BitmapText;
-import com.jme3.input.MouseInput;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.MouseButtonTrigger;
-import com.jme3.material.Material;
-import com.jme3.material.RenderState;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Vector2f;
-import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Node;
-import com.jme3.scene.shape.Quad;
-
-public class PauseMenu extends BaseAppState {
-
- private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
- private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
- private static final ColorRGBA COL_BTN = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
- private static final ColorRGBA COL_BTN_DIS = new ColorRGBA(0.12f, 0.12f, 0.18f, 1.00f);
- private static final ColorRGBA COL_BTN_QUIT = new ColorRGBA(0.38f, 0.10f, 0.10f, 1.00f);
- private static final ColorRGBA COL_TEXT = ColorRGBA.White;
- private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f);
- private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f);
-
- private static final int BTN_GRAFIK = 0;
- private static final int BTN_AUDIO = 1;
- private static final int BTN_STEUERUNG = 2;
- private static final int BTN_BEENDEN = 3;
-
- private SimpleApplication app;
- private Node guiNode;
- private BitmapFont font;
- private Node panel;
-
- private Runnable onGraphics;
- private Runnable onControls;
-
- // [x, y, w, h] per button
- private final float[][] btnBounds = new float[4][4];
-
- public PauseMenu(Runnable onGraphics, Runnable onControls) {
- this.onGraphics = onGraphics;
- this.onControls = onControls;
- }
-
- @Override
- protected void initialize(Application app) {
- this.app = (SimpleApplication) app;
- this.guiNode = this.app.getGuiNode();
- this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
- }
-
- @Override
- protected void onEnable() {
- buildUI();
- app.getInputManager().setCursorVisible(true);
- app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
- app.getInputManager().addListener(clickListener, "_PauseClick");
- }
-
- @Override
- protected void onDisable() {
- if (panel != null) { guiNode.detachChild(panel); panel = null; }
- app.getInputManager().deleteMapping("_PauseClick");
- app.getInputManager().setCursorVisible(false);
- }
-
- @Override protected void cleanup(Application app) {}
-
- private void buildUI() {
- float sw = app.getCamera().getWidth();
- float sh = app.getCamera().getHeight();
- panel = new Node("pause-panel");
- addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
-
- float pw = 320, ph = 360;
- float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
- addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
-
- BitmapText title = txt("PAUSE", 26, COL_TEXT);
- centerText(title, px, py + ph - 48, pw);
- panel.attachChild(title);
-
- String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"};
- boolean[] enabled = {true, false, true, true};
- float bw = 260, bh = 52;
- float bx = px + (pw - bw) / 2f;
- float startY = py + ph - 112;
- float step = 62;
-
- for (int i = 0; i < 4; i++) {
- float by = startY - i * step;
- ColorRGBA bgCol = !enabled[i] ? COL_BTN_DIS : (i == BTN_BEENDEN ? COL_BTN_QUIT : COL_BTN);
- ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS;
-
- addQuad(panel, bx, by, bw, bh, bgCol, 0);
-
- BitmapText lbl = txt(labels[i], 18, txCol);
- if (!enabled[i]) {
- // Center label in upper portion, show hint below
- lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1);
- BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB);
- hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1);
- panel.attachChild(hint);
- } else {
- centerText(lbl, bx, by + bh - 16, bw);
- }
- panel.attachChild(lbl);
-
- btnBounds[i][0] = bx; btnBounds[i][1] = by;
- btnBounds[i][2] = bw; btnBounds[i][3] = bh;
- }
-
- guiNode.attachChild(panel);
- }
-
- private final ActionListener clickListener = (name, isPressed, tpf) -> {
- if (!isPressed) return;
- Vector2f c = app.getInputManager().getCursorPosition();
-
- for (int i = 0; i < 4; i++) {
- if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
- switch (i) {
- case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
- case BTN_AUDIO -> { /* Bald verfügbar */ }
- case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
- case BTN_BEENDEN -> app.stop();
- }
- return;
- }
- };
-
- // -----------------------------------------------------------------------
-
- private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
- Geometry geo = new Geometry("q", new Quad(w, h));
- Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
- mat.setColor("Color", color.clone());
- if (color.a < 1f) {
- mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
- geo.setQueueBucket(RenderQueue.Bucket.Transparent);
- }
- geo.setMaterial(mat);
- geo.setLocalTranslation(x, y, z);
- parent.attachChild(geo);
- return geo;
- }
-
- private BitmapText txt(String s, int size, ColorRGBA color) {
- BitmapText t = new BitmapText(font, false);
- t.setSize(size); t.setColor(color); t.setText(s);
- return t;
- }
-
- private void centerText(BitmapText t, float x, float y, float width) {
- t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
- }
-
- private boolean hits(Vector2f p, float x, float y, float w, float h) {
- return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
- }
-}
+package de.blight.game.config;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.MouseButtonTrigger;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector2f;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Node;
+import com.jme3.scene.shape.Quad;
+
+public class PauseMenu extends BaseAppState {
+
+ private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
+ private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
+ private static final ColorRGBA COL_BTN = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
+ private static final ColorRGBA COL_BTN_DIS = new ColorRGBA(0.12f, 0.12f, 0.18f, 1.00f);
+ private static final ColorRGBA COL_BTN_QUIT = new ColorRGBA(0.38f, 0.10f, 0.10f, 1.00f);
+ private static final ColorRGBA COL_TEXT = ColorRGBA.White;
+ private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f);
+ private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f);
+
+ private static final int BTN_GRAFIK = 0;
+ private static final int BTN_AUDIO = 1;
+ private static final int BTN_STEUERUNG = 2;
+ private static final int BTN_BEENDEN = 3;
+
+ private SimpleApplication app;
+ private Node guiNode;
+ private BitmapFont font;
+ private Node panel;
+
+ private Runnable onGraphics;
+ private Runnable onControls;
+
+ // [x, y, w, h] per button
+ private final float[][] btnBounds = new float[4][4];
+
+ public PauseMenu(Runnable onGraphics, Runnable onControls) {
+ this.onGraphics = onGraphics;
+ this.onControls = onControls;
+ }
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.guiNode = this.app.getGuiNode();
+ this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
+ }
+
+ @Override
+ protected void onEnable() {
+ buildUI();
+ app.getInputManager().setCursorVisible(true);
+ app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
+ app.getInputManager().addListener(clickListener, "_PauseClick");
+ }
+
+ @Override
+ protected void onDisable() {
+ if (panel != null) { guiNode.detachChild(panel); panel = null; }
+ app.getInputManager().deleteMapping("_PauseClick");
+ app.getInputManager().setCursorVisible(false);
+ }
+
+ @Override protected void cleanup(Application app) {}
+
+ private void buildUI() {
+ float sw = app.getCamera().getWidth();
+ float sh = app.getCamera().getHeight();
+ panel = new Node("pause-panel");
+ addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
+
+ float pw = 320, ph = 360;
+ float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
+ addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
+
+ BitmapText title = txt("PAUSE", 26, COL_TEXT);
+ centerText(title, px, py + ph - 48, pw);
+ panel.attachChild(title);
+
+ String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"};
+ boolean[] enabled = {true, false, true, true};
+ float bw = 260, bh = 52;
+ float bx = px + (pw - bw) / 2f;
+ float startY = py + ph - 112;
+ float step = 62;
+
+ for (int i = 0; i < 4; i++) {
+ float by = startY - i * step;
+ ColorRGBA bgCol = !enabled[i] ? COL_BTN_DIS : (i == BTN_BEENDEN ? COL_BTN_QUIT : COL_BTN);
+ ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS;
+
+ addQuad(panel, bx, by, bw, bh, bgCol, 0);
+
+ BitmapText lbl = txt(labels[i], 18, txCol);
+ if (!enabled[i]) {
+ // Center label in upper portion, show hint below
+ lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1);
+ BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB);
+ hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1);
+ panel.attachChild(hint);
+ } else {
+ centerText(lbl, bx, by + bh - 16, bw);
+ }
+ panel.attachChild(lbl);
+
+ btnBounds[i][0] = bx; btnBounds[i][1] = by;
+ btnBounds[i][2] = bw; btnBounds[i][3] = bh;
+ }
+
+ guiNode.attachChild(panel);
+ }
+
+ private final ActionListener clickListener = (name, isPressed, tpf) -> {
+ if (!isPressed) return;
+ Vector2f c = app.getInputManager().getCursorPosition();
+
+ for (int i = 0; i < 4; i++) {
+ if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
+ switch (i) {
+ case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
+ case BTN_AUDIO -> { /* Bald verfügbar */ }
+ case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
+ case BTN_BEENDEN -> app.stop();
+ }
+ return;
+ }
+ };
+
+ // -----------------------------------------------------------------------
+
+ private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
+ Geometry geo = new Geometry("q", new Quad(w, h));
+ Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", color.clone());
+ if (color.a < 1f) {
+ mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
+ geo.setQueueBucket(RenderQueue.Bucket.Transparent);
+ }
+ geo.setMaterial(mat);
+ geo.setLocalTranslation(x, y, z);
+ parent.attachChild(geo);
+ return geo;
+ }
+
+ private BitmapText txt(String s, int size, ColorRGBA color) {
+ BitmapText t = new BitmapText(font, false);
+ t.setSize(size); t.setColor(color); t.setText(s);
+ return t;
+ }
+
+ private void centerText(BitmapText t, float x, float y, float width) {
+ t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
+ }
+
+ private boolean hits(Vector2f p, float x, float y, float w, float h) {
+ return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java
index 97c4512..ff48f27 100644
--- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java
+++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java
@@ -1,113 +1,113 @@
-package de.blight.game.control;
-
-import com.jme3.bullet.control.CharacterControl;
-import com.jme3.input.InputManager;
-import com.jme3.input.controls.ActionListener;
-import com.jme3.input.controls.KeyTrigger;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Vector3f;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.Spatial;
-import de.blight.game.config.KeyBindings;
-
-public class PlayerInputControl {
-
- private static final float MOVE_SPEED = 0.07f;
- private static final float SPRINT_MULT = 1.5f;
- private static final float ROTATE_SPEED = 10f;
-
- private static final String[] ACTION_NAMES =
- {"Forward", "Backward", "Left", "Right", "Jump", "Sprint"};
-
- private final InputManager inputManager;
- private final Camera cam;
-
- private CharacterControl physicsChar;
- private Spatial visual;
-
- private boolean forward, backward, left, right, sprint;
- private boolean paused = false;
-
- // Listener als Feld, damit er bei reload nicht doppelt registriert wird
- private final ActionListener actionListener = (name, isPressed, tpf) -> {
- if (paused) return;
- switch (name) {
- case "Forward" -> forward = isPressed;
- case "Backward" -> backward = isPressed;
- case "Left" -> left = isPressed;
- case "Right" -> right = isPressed;
- case "Sprint" -> sprint = isPressed;
- case "Jump" -> { if (isPressed && physicsChar != null) physicsChar.jump(); }
- }
- };
-
- public PlayerInputControl(InputManager inputManager, Camera cam, KeyBindings kb) {
- this.inputManager = inputManager;
- this.cam = cam;
- registerMappings(kb);
- }
-
- public void setPhysicsCharacter(CharacterControl physicsChar) {
- this.physicsChar = physicsChar;
- }
-
- public void setVisual(Spatial visual) {
- this.visual = visual;
- }
-
- public void setPaused(boolean paused) {
- this.paused = paused;
- if (paused) {
- forward = backward = left = right = sprint = false;
- if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
- }
- }
-
- /** Löscht alte Mappings und registriert neue aus den übergebenen KeyBindings. */
- public void reloadBindings(KeyBindings kb) {
- for (String a : ACTION_NAMES) inputManager.deleteMapping(a);
- registerMappings(kb);
- // Zustand zurücksetzen, damit keine Taste „hängt"
- forward = backward = left = right = sprint = false;
- if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
- }
-
- private void registerMappings(KeyBindings kb) {
- inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
- inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
- inputManager.addMapping("Left", new KeyTrigger(kb.left));
- inputManager.addMapping("Right", new KeyTrigger(kb.right));
- inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
- inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
- inputManager.addListener(actionListener, ACTION_NAMES);
- }
-
- public void update(float tpf) {
- if (physicsChar == null || paused) return;
-
- Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
- Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
-
- Vector3f moveDir = new Vector3f();
- if (forward) moveDir.addLocal(camDir);
- if (backward) moveDir.subtractLocal(camDir);
- if (left) moveDir.addLocal(camLeft);
- if (right) moveDir.subtractLocal(camLeft);
-
- if (moveDir.lengthSquared() > 0.001f) {
- moveDir.normalizeLocal();
- float speed = sprint ? MOVE_SPEED * SPRINT_MULT : MOVE_SPEED;
- physicsChar.setWalkDirection(moveDir.mult(speed));
-
- if (visual != null) {
- Quaternion targetRot = new Quaternion();
- targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
- Quaternion current = visual.getLocalRotation().clone();
- current.slerp(targetRot, ROTATE_SPEED * tpf);
- visual.setLocalRotation(current);
- }
- } else {
- physicsChar.setWalkDirection(Vector3f.ZERO);
- }
- }
-}
+package de.blight.game.control;
+
+import com.jme3.bullet.control.CharacterControl;
+import com.jme3.input.InputManager;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.Spatial;
+import de.blight.game.config.KeyBindings;
+
+public class PlayerInputControl {
+
+ private static final float MOVE_SPEED = 0.07f;
+ private static final float SPRINT_MULT = 1.5f;
+ private static final float ROTATE_SPEED = 10f;
+
+ private static final String[] ACTION_NAMES =
+ {"Forward", "Backward", "Left", "Right", "Jump", "Sprint"};
+
+ private final InputManager inputManager;
+ private final Camera cam;
+
+ private CharacterControl physicsChar;
+ private Spatial visual;
+
+ private boolean forward, backward, left, right, sprint;
+ private boolean paused = false;
+
+ // Listener als Feld, damit er bei reload nicht doppelt registriert wird
+ private final ActionListener actionListener = (name, isPressed, tpf) -> {
+ if (paused) return;
+ switch (name) {
+ case "Forward" -> forward = isPressed;
+ case "Backward" -> backward = isPressed;
+ case "Left" -> left = isPressed;
+ case "Right" -> right = isPressed;
+ case "Sprint" -> sprint = isPressed;
+ case "Jump" -> { if (isPressed && physicsChar != null) physicsChar.jump(); }
+ }
+ };
+
+ public PlayerInputControl(InputManager inputManager, Camera cam, KeyBindings kb) {
+ this.inputManager = inputManager;
+ this.cam = cam;
+ registerMappings(kb);
+ }
+
+ public void setPhysicsCharacter(CharacterControl physicsChar) {
+ this.physicsChar = physicsChar;
+ }
+
+ public void setVisual(Spatial visual) {
+ this.visual = visual;
+ }
+
+ public void setPaused(boolean paused) {
+ this.paused = paused;
+ if (paused) {
+ forward = backward = left = right = sprint = false;
+ if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
+ }
+ }
+
+ /** Löscht alte Mappings und registriert neue aus den übergebenen KeyBindings. */
+ public void reloadBindings(KeyBindings kb) {
+ for (String a : ACTION_NAMES) inputManager.deleteMapping(a);
+ registerMappings(kb);
+ // Zustand zurücksetzen, damit keine Taste „hängt"
+ forward = backward = left = right = sprint = false;
+ if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
+ }
+
+ private void registerMappings(KeyBindings kb) {
+ inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
+ inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
+ inputManager.addMapping("Left", new KeyTrigger(kb.left));
+ inputManager.addMapping("Right", new KeyTrigger(kb.right));
+ inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
+ inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
+ inputManager.addListener(actionListener, ACTION_NAMES);
+ }
+
+ public void update(float tpf) {
+ if (physicsChar == null || paused) return;
+
+ Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
+ Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
+
+ Vector3f moveDir = new Vector3f();
+ if (forward) moveDir.addLocal(camDir);
+ if (backward) moveDir.subtractLocal(camDir);
+ if (left) moveDir.addLocal(camLeft);
+ if (right) moveDir.subtractLocal(camLeft);
+
+ if (moveDir.lengthSquared() > 0.001f) {
+ moveDir.normalizeLocal();
+ float speed = sprint ? MOVE_SPEED * SPRINT_MULT : MOVE_SPEED;
+ physicsChar.setWalkDirection(moveDir.mult(speed));
+
+ if (visual != null) {
+ Quaternion targetRot = new Quaternion();
+ targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
+ Quaternion current = visual.getLocalRotation().clone();
+ current.slerp(targetRot, ROTATE_SPEED * tpf);
+ visual.setLocalRotation(current);
+ }
+ } else {
+ physicsChar.setWalkDirection(Vector3f.ZERO);
+ }
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java
index c1610a6..b107c89 100644
--- a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java
+++ b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java
@@ -1,93 +1,93 @@
-package de.blight.game.control;
-
-import com.jme3.input.InputManager;
-import com.jme3.input.MouseInput;
-import com.jme3.input.controls.AnalogListener;
-import com.jme3.input.controls.MouseAxisTrigger;
-import com.jme3.math.*;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.Spatial;
-
-/**
- * Third-Person-Kamera:
- * - Mausbewegung → Kamera um den Charakter drehen (immer, kein Klick nötig)
- * - Y-Achse invertiert → Maus hoch = Kamera runter (oldschool)
- * - Mausrad → Zoom (Abstand)
- */
-public class ThirdPersonCamera {
-
- private static final float MOUSE_SENSITIVITY = 1.8f;
- private static final float MIN_DISTANCE = 3f;
- private static final float MAX_DISTANCE = 20f;
- private static final float MIN_VERTICAL_ANGLE = -0.3f;
- private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
- private static final float TARGET_HEIGHT = 1.6f;
-
- private final Camera cam;
- private final InputManager inputManager;
-
- private Spatial target;
-
- private float yaw = 0f;
- private float pitch = 0.4f;
- private float distance = 10f;
- private boolean paused = false;
-
- public ThirdPersonCamera(Camera cam, InputManager inputManager) {
- this.cam = cam;
- this.inputManager = inputManager;
- registerMappings();
- }
-
- public void setTarget(Spatial target) {
- this.target = target;
- }
-
- public void setPaused(boolean paused) { this.paused = paused; }
-
- // -----------------------------------------------------------------------
-
- private void registerMappings() {
- inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
- inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
- inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false));
- inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true));
- inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
- inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
-
- AnalogListener analogListener = (name, value, tpf) -> {
- if (paused) return;
- switch (name) {
- // Horizontale Rotation
- case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
- case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
- // Vertikale Rotation — Y invertiert: Maus hoch → Kamera runter
- case "MouseY" -> pitch = FastMath.clamp(pitch - value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
- case "MouseYNeg" -> pitch = FastMath.clamp(pitch + value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
- // Zoom
- case "ZoomIn" -> distance = FastMath.clamp(distance - value * 20f, MIN_DISTANCE, MAX_DISTANCE);
- case "ZoomOut" -> distance = FastMath.clamp(distance + value * 20f, MIN_DISTANCE, MAX_DISTANCE);
- }
- };
- inputManager.addListener(analogListener,
- "MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut");
- }
-
- public void update(float tpf) {
- if (target == null) return;
-
- Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0);
-
- // Sphärische Koordinaten → kartesisch
- float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw);
- float y = distance * FastMath.sin(pitch);
- float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw);
-
- Vector3f camPos = pivot.add(x, y, z);
- cam.setLocation(camPos);
- cam.lookAt(pivot, Vector3f.UNIT_Y);
- }
-
- /** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
- public float getYaw() { return yaw; }
-}
+package de.blight.game.control;
+
+import com.jme3.input.InputManager;
+import com.jme3.input.MouseInput;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.MouseAxisTrigger;
+import com.jme3.math.*;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.Spatial;
+
+/**
+ * Third-Person-Kamera:
+ * - Mausbewegung → Kamera um den Charakter drehen (immer, kein Klick nötig)
+ * - Y-Achse invertiert → Maus hoch = Kamera runter (oldschool)
+ * - Mausrad → Zoom (Abstand)
+ */
+public class ThirdPersonCamera {
+
+ private static final float MOUSE_SENSITIVITY = 1.8f;
+ private static final float MIN_DISTANCE = 3f;
+ private static final float MAX_DISTANCE = 20f;
+ private static final float MIN_VERTICAL_ANGLE = -0.3f;
+ private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
+ private static final float TARGET_HEIGHT = 1.6f;
+
+ private final Camera cam;
+ private final InputManager inputManager;
+
+ private Spatial target;
+
+ private float yaw = 0f;
+ private float pitch = 0.4f;
+ private float distance = 10f;
+ private boolean paused = false;
+
+ public ThirdPersonCamera(Camera cam, InputManager inputManager) {
+ this.cam = cam;
+ this.inputManager = inputManager;
+ registerMappings();
+ }
+
+ public void setTarget(Spatial target) {
+ this.target = target;
+ }
+
+ public void setPaused(boolean paused) { this.paused = paused; }
+
+ // -----------------------------------------------------------------------
+
+ private void registerMappings() {
+ inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
+ inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
+ inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false));
+ inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true));
+ inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
+ inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
+
+ AnalogListener analogListener = (name, value, tpf) -> {
+ if (paused) return;
+ switch (name) {
+ // Horizontale Rotation
+ case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
+ case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
+ // Vertikale Rotation — Y invertiert: Maus hoch → Kamera runter
+ case "MouseY" -> pitch = FastMath.clamp(pitch - value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
+ case "MouseYNeg" -> pitch = FastMath.clamp(pitch + value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
+ // Zoom
+ case "ZoomIn" -> distance = FastMath.clamp(distance - value * 20f, MIN_DISTANCE, MAX_DISTANCE);
+ case "ZoomOut" -> distance = FastMath.clamp(distance + value * 20f, MIN_DISTANCE, MAX_DISTANCE);
+ }
+ };
+ inputManager.addListener(analogListener,
+ "MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut");
+ }
+
+ public void update(float tpf) {
+ if (target == null) return;
+
+ Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0);
+
+ // Sphärische Koordinaten → kartesisch
+ float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw);
+ float y = distance * FastMath.sin(pitch);
+ float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw);
+
+ Vector3f camPos = pivot.add(x, y, z);
+ cam.setLocation(camPos);
+ cam.lookAt(pivot, Vector3f.UNIT_Y);
+ }
+
+ /** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
+ public float getYaw() { return yaw; }
+}
diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java
index 037fffd..5ab13dd 100644
--- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java
+++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java
@@ -1,308 +1,388 @@
-package de.blight.game.scene;
-
-import com.jme3.app.Application;
-import com.jme3.app.SimpleApplication;
-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.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.math.*;
-import com.jme3.renderer.queue.RenderQueue;
-import com.jme3.scene.*;
-import com.jme3.scene.shape.*;
-import com.jme3.shadow.*;
-import com.jme3.terrain.geomipmap.*;
-import com.jme3.texture.*;
-import com.jme3.util.SkyFactory;
-import de.blight.game.config.KeyBindings;
-import de.blight.game.control.PlayerInputControl;
-import de.blight.game.control.ThirdPersonCamera;
-
-public class WorldScene extends BaseAppState {
-
- private SimpleApplication app;
- private Node rootNode;
- private AssetManager assetManager;
- private BulletAppState bulletAppState;
-
- private final KeyBindings keyBindings;
- private ThirdPersonCamera thirdPersonCam;
- private PlayerInputControl playerInput;
-
- public WorldScene(KeyBindings keyBindings) {
- this.keyBindings = keyBindings;
- }
-
- /** Wird von ConfigScreen nach dem Speichern aufgerufen. */
- public void reloadBindings(KeyBindings kb) {
- if (playerInput != null) playerInput.reloadBindings(kb);
- }
-
- public void setPaused(boolean paused) {
- if (playerInput != null) playerInput.setPaused(paused);
- if (thirdPersonCam != null) thirdPersonCam.setPaused(paused);
- }
-
- // -----------------------------------------------------------------------
- // Lifecycle
- // -----------------------------------------------------------------------
-
- @Override
- protected void initialize(Application app) {
- this.app = (SimpleApplication) app;
- this.rootNode = this.app.getRootNode();
- this.assetManager = app.getAssetManager();
-
- bulletAppState = new BulletAppState();
- app.getStateManager().attach(bulletAppState);
- }
-
- @Override
- protected void onEnable() {
- buildLighting();
- TerrainQuad terrain = buildTerrain();
- buildDecorations(terrain);
-
- Node character = buildCharacter();
- rootNode.attachChild(character);
-
- // Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1)
- CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1);
- CharacterControl physicsChar = new CharacterControl(capsule, 0.05f);
- physicsChar.setJumpSpeed(12f);
- physicsChar.setFallSpeed(35f);
- physicsChar.setGravity(35f);
- physicsChar.setPhysicsLocation(new Vector3f(0, 5f, 0));
- character.addControl(physicsChar);
- bulletAppState.getPhysicsSpace().add(physicsChar);
-
- playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings);
- playerInput.setPhysicsCharacter(physicsChar);
- playerInput.setVisual(character);
-
- thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
- thirdPersonCam.setTarget(character);
-
- // Maus einfangen – keine Klick-Pflicht für Kamerasteuerung
- app.getInputManager().setCursorVisible(false);
- }
-
- @Override
- public void update(float tpf) {
- playerInput.update(tpf);
- thirdPersonCam.update(tpf);
- }
-
- @Override protected void cleanup(Application app) {}
- @Override protected void onDisable() {}
-
- // -----------------------------------------------------------------------
- // Beleuchtung
- // -----------------------------------------------------------------------
-
- private void buildLighting() {
- DirectionalLight sun = new DirectionalLight();
- sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
- sun.setColor(ColorRGBA.White.mult(1.4f));
- rootNode.addLight(sun);
-
- AmbientLight ambient = new AmbientLight();
- ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f));
- rootNode.addLight(ambient);
-
- DirectionalLightShadowRenderer shadowRenderer =
- new DirectionalLightShadowRenderer(assetManager, 2048, 3);
- shadowRenderer.setLight(sun);
- shadowRenderer.setShadowIntensity(0.4f);
- app.getViewPort().addProcessor(shadowRenderer);
-
- try {
- Spatial sky = SkyFactory.createSky(assetManager,
- "Textures/Sky/Bright/BrightSky.dds",
- SkyFactory.EnvMapType.CubeMap);
- rootNode.attachChild(sky);
- } catch (Exception ignored) {}
- }
-
- // -----------------------------------------------------------------------
- // Terrain
- // -----------------------------------------------------------------------
-
- private TerrainQuad buildTerrain() {
- int size = 257;
- float[] heights = new float[size * size];
-
- for (int z = 0; z < size; z++) {
- for (int x = 0; x < size; x++) {
- float nx = x / (float) size;
- float nz = z / (float) size;
- heights[z * size + x] =
- FastMath.sin(nx * FastMath.TWO_PI * 2) * 2f
- + FastMath.sin(nz * FastMath.TWO_PI * 3) * 1.5f
- + FastMath.sin((nx + nz) * FastMath.TWO_PI * 1.5f) * 1f;
- }
- }
-
- TerrainQuad terrain = new TerrainQuad("terrain", 65, size, heights);
-
- Material terrainMat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
- Texture grass = assetManager.loadTexture("Textures/gras.png");
- grass.setWrap(Texture.WrapMode.Repeat);
- terrainMat.setTexture("Tex1", grass);
- terrainMat.setFloat("Tex1Scale", 64f);
-
- terrain.setMaterial(terrainMat);
- terrain.setLocalTranslation(0, -5f, 0);
- terrain.setLocalScale(0.5f, 0.5f, 0.5f);
- terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
-
- // Statischer Physics-Body (mass=0) für Terrain-Kollision
- RigidBodyControl terrainPhysics = new RigidBodyControl(
- CollisionShapeFactory.createMeshShape(terrain), 0f);
- terrain.addControl(terrainPhysics);
- bulletAppState.getPhysicsSpace().add(terrainPhysics);
-
- rootNode.attachChild(terrain);
- return terrain;
- }
-
- // -----------------------------------------------------------------------
- // Dekorationen – Höhe per TerrainQuad.getHeight() anpassen
- // -----------------------------------------------------------------------
-
- private void buildDecorations(TerrainQuad terrain) {
- Material stoneMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
- stoneMat.setBoolean("UseMaterialColors", true);
- stoneMat.setColor("Diffuse", new ColorRGBA(0.55f, 0.55f, 0.55f, 1f));
- stoneMat.setColor("Ambient", new ColorRGBA(0.3f, 0.3f, 0.3f, 1f));
- stoneMat.setColor("Specular", ColorRGBA.White);
- stoneMat.setFloat("Shininess", 32f);
-
- Material treeTrunkMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
- treeTrunkMat.setBoolean("UseMaterialColors", true);
- treeTrunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.28f, 0.1f, 1f));
- treeTrunkMat.setColor("Ambient", new ColorRGBA(0.2f, 0.12f, 0.04f, 1f));
- treeTrunkMat.setColor("Specular", ColorRGBA.Black);
-
- Material treeTopMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
- treeTopMat.setBoolean("UseMaterialColors", true);
- treeTopMat.setColor("Diffuse", new ColorRGBA(0.1f, 0.55f, 0.15f, 1f));
- treeTopMat.setColor("Ambient", new ColorRGBA(0.05f, 0.25f, 0.07f, 1f));
- treeTopMat.setColor("Specular", ColorRGBA.Black);
-
- float[][] treeXZ = {
- {12, 8}, {-15, 5}, {20, -10}, {-8, -18},
- {5, 25}, {-22, 12}, {18, 20}, {-10, -5}
- };
- for (float[] xz : treeXZ) {
- float worldY = terrainWorldY(terrain, xz[0], xz[1]);
- Node tree = new Node("tree");
-
- Geometry trunk = new Geometry("trunk", new Cylinder(8, 8, 0.25f, 2.5f, true));
- trunk.setMaterial(treeTrunkMat);
- trunk.rotate(FastMath.HALF_PI, 0, 0);
- trunk.setLocalTranslation(0, 1.25f, 0);
- trunk.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
-
- Geometry crown = new Geometry("crown", new Sphere(12, 12, 2.2f));
- crown.setMaterial(treeTopMat);
- crown.setLocalTranslation(0, 3.8f, 0);
- crown.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
-
- tree.attachChild(trunk);
- tree.attachChild(crown);
- tree.setLocalTranslation(xz[0], worldY, xz[1]);
- rootNode.attachChild(tree);
- }
-
- float[][] stoneXZ = {{6, -6}, {-12, 15}, {16, -4}, {-3, 10}};
- for (float[] xz : stoneXZ) {
- float worldY = terrainWorldY(terrain, xz[0], xz[1]);
- float r = 0.6f + FastMath.nextRandomFloat() * 0.8f;
- Geometry stone = new Geometry("stone", new Sphere(8, 8, r));
- stone.setMaterial(stoneMat);
- stone.setLocalTranslation(xz[0], worldY + r * 0.5f, xz[1]);
- stone.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
- rootNode.attachChild(stone);
- }
- }
-
- /** Konvertiert Welt-XZ in lokale Terrain-Koordinaten und fragt die Höhe ab. */
- private float terrainWorldY(TerrainQuad terrain, float worldX, float worldZ) {
- Vector3f terrainTranslation = terrain.getWorldTranslation();
- Vector3f terrainScale = terrain.getWorldScale();
- float localX = (worldX - terrainTranslation.x) / terrainScale.x;
- float localZ = (worldZ - terrainTranslation.z) / terrainScale.z;
- float localH = terrain.getHeight(new Vector2f(localX, localZ));
- return terrainTranslation.y + localH * terrainScale.y;
- }
-
- // -----------------------------------------------------------------------
- // Charakter (visueller Node, ohne eigenes Mesh für Physics)
- // -----------------------------------------------------------------------
-
- private Node buildCharacter() {
- Node character = new Node("character");
-
- Material bodyMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
- bodyMat.setBoolean("UseMaterialColors", true);
- bodyMat.setColor("Diffuse", new ColorRGBA(0.2f, 0.4f, 0.8f, 1f));
- bodyMat.setColor("Ambient", new ColorRGBA(0.1f, 0.2f, 0.4f, 1f));
- bodyMat.setColor("Specular", ColorRGBA.White);
- bodyMat.setFloat("Shininess", 64f);
-
- Material headMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
- headMat.setBoolean("UseMaterialColors", true);
- headMat.setColor("Diffuse", new ColorRGBA(0.9f, 0.75f, 0.6f, 1f));
- headMat.setColor("Ambient", new ColorRGBA(0.45f, 0.37f, 0.3f, 1f));
- headMat.setColor("Specular", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f));
- headMat.setFloat("Shininess", 16f);
-
- Geometry body = new Geometry("body", new Cylinder(8, 16, 0.35f, 1.1f, true));
- body.setMaterial(bodyMat);
- body.rotate(FastMath.HALF_PI, 0, 0);
- body.setLocalTranslation(0, 0.9f, 0);
- body.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
-
- Geometry head = new Geometry("head", new Sphere(16, 16, 0.28f));
- head.setMaterial(headMat);
- head.setLocalTranslation(0, 1.75f, 0);
- head.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
-
- Geometry armL = buildLimb(bodyMat, 0.1f, 0.55f);
- armL.setLocalTranslation(-0.5f, 0.85f, 0);
- armL.rotate(0, 0, FastMath.DEG_TO_RAD * 20f);
-
- Geometry armR = buildLimb(bodyMat, 0.1f, 0.55f);
- armR.setLocalTranslation(0.5f, 0.85f, 0);
- armR.rotate(0, 0, FastMath.DEG_TO_RAD * -20f);
-
- Geometry legL = buildLimb(bodyMat, 0.12f, 0.6f);
- legL.setLocalTranslation(-0.18f, 0.3f, 0);
-
- Geometry legR = buildLimb(bodyMat, 0.12f, 0.6f);
- legR.setLocalTranslation(0.18f, 0.3f, 0);
-
- character.attachChild(body);
- character.attachChild(head);
- character.attachChild(armL);
- character.attachChild(armR);
- character.attachChild(legL);
- character.attachChild(legR);
-
- return character;
- }
-
- private Geometry buildLimb(Material mat, float radius, float height) {
- Geometry limb = new Geometry("limb", new Cylinder(6, 12, radius, height, true));
- limb.setMaterial(mat);
- limb.rotate(FastMath.HALF_PI, 0, 0);
- limb.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
- return limb;
- }
-}
+package de.blight.game.scene;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+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.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.math.*;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.*;
+import com.jme3.scene.shape.*;
+import com.jme3.shadow.*;
+import com.jme3.terrain.geomipmap.*;
+import com.jme3.texture.*;
+import com.jme3.util.SkyFactory;
+import de.blight.common.MapData;
+import de.blight.common.MapIO;
+import de.blight.game.config.KeyBindings;
+import de.blight.game.control.PlayerInputControl;
+import de.blight.game.control.ThirdPersonCamera;
+import de.blight.game.state.GrassState;
+
+import java.io.IOException;
+
+public class WorldScene extends BaseAppState {
+
+ private SimpleApplication app;
+ private Node rootNode;
+ private AssetManager assetManager;
+ private BulletAppState bulletAppState;
+ private MapData loadedMapData;
+
+ private final KeyBindings keyBindings;
+ private ThirdPersonCamera thirdPersonCam;
+ private PlayerInputControl playerInput;
+ private float spawnY = 5f; // wird in buildTerrain() gesetzt
+
+ public WorldScene(KeyBindings keyBindings) {
+ this.keyBindings = keyBindings;
+ }
+
+ /** Wird von ConfigScreen nach dem Speichern aufgerufen. */
+ public void reloadBindings(KeyBindings kb) {
+ if (playerInput != null) playerInput.reloadBindings(kb);
+ }
+
+ public void setPaused(boolean paused) {
+ if (playerInput != null) playerInput.setPaused(paused);
+ if (thirdPersonCam != null) thirdPersonCam.setPaused(paused);
+ }
+
+ // -----------------------------------------------------------------------
+ // Lifecycle
+ // -----------------------------------------------------------------------
+
+ @Override
+ protected void initialize(Application app) {
+ this.app = (SimpleApplication) app;
+ this.rootNode = this.app.getRootNode();
+ this.assetManager = app.getAssetManager();
+
+ bulletAppState = new BulletAppState();
+ app.getStateManager().attach(bulletAppState);
+ }
+
+ @Override
+ protected void onEnable() {
+ buildLighting();
+ TerrainQuad terrain = buildTerrain();
+ buildDecorations(terrain);
+
+ if (loadedMapData != null) {
+ app.getStateManager().attach(new GrassState(loadedMapData, terrain));
+ }
+
+ Node character = buildCharacter();
+ rootNode.attachChild(character);
+
+ // Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1)
+ CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1);
+ CharacterControl physicsChar = new CharacterControl(capsule, 0.05f);
+ physicsChar.setJumpSpeed(12f);
+ physicsChar.setFallSpeed(35f);
+ physicsChar.setGravity(35f);
+ physicsChar.setPhysicsLocation(new Vector3f(0, spawnY, 0));
+ character.addControl(physicsChar);
+ bulletAppState.getPhysicsSpace().add(physicsChar);
+
+ playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings);
+ playerInput.setPhysicsCharacter(physicsChar);
+ playerInput.setVisual(character);
+
+ thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
+ thirdPersonCam.setTarget(character);
+
+ // Maus einfangen – keine Klick-Pflicht für Kamerasteuerung
+ app.getInputManager().setCursorVisible(false);
+ }
+
+ @Override
+ public void update(float tpf) {
+ playerInput.update(tpf);
+ thirdPersonCam.update(tpf);
+ }
+
+ @Override protected void cleanup(Application app) {}
+ @Override protected void onDisable() {}
+
+ // -----------------------------------------------------------------------
+ // Beleuchtung
+ // -----------------------------------------------------------------------
+
+ private void buildLighting() {
+ DirectionalLight sun = new DirectionalLight();
+ sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
+ sun.setColor(ColorRGBA.White.mult(1.4f));
+ rootNode.addLight(sun);
+
+ AmbientLight ambient = new AmbientLight();
+ ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f));
+ rootNode.addLight(ambient);
+
+ DirectionalLightShadowRenderer shadowRenderer =
+ new DirectionalLightShadowRenderer(assetManager, 2048, 3);
+ shadowRenderer.setLight(sun);
+ shadowRenderer.setShadowIntensity(0.4f);
+ app.getViewPort().addProcessor(shadowRenderer);
+
+ try {
+ Spatial sky = SkyFactory.createSky(assetManager,
+ "Textures/Sky/Bright/BrightSky.dds",
+ SkyFactory.EnvMapType.CubeMap);
+ rootNode.attachChild(sky);
+ } catch (Exception ignored) {}
+ }
+
+ // -----------------------------------------------------------------------
+ // Terrain
+ // -----------------------------------------------------------------------
+
+ /**
+ * Baut das Terrain. Falls eine gespeicherte Karte vorhanden ist, wird diese
+ * geladen; andernfalls wird ein prozedurales Demo-Terrain erzeugt.
+ * Setzt außerdem {@link #spawnY}.
+ */
+ private TerrainQuad buildTerrain() {
+ if (MapIO.exists()) {
+ try {
+ loadedMapData = MapIO.load();
+ return buildTerrainFromMap(loadedMapData);
+ } catch (IOException e) {
+ System.err.println("[WorldScene] Karte nicht ladbar: " + e.getMessage()
+ + " — Fallback auf prozedurales Terrain.");
+ }
+ }
+ return buildProceduralTerrain();
+ }
+
+ /**
+ * Erstellt ein Terrain aus der gespeicherten {@link MapData}.
+ * Die 4097×4097 Editor-Daten werden auf 513×513 heruntergesampelt
+ * (jeder 8. Vertex), mit Scale (8, 1, 8) auf die gleiche Weltgröße
+ * 4096 × 4096 WE gebracht.
+ */
+ private TerrainQuad buildTerrainFromMap(MapData map) {
+ final int GAME_VERTS = 513; // 512 Zellen à 8 WE = 4096 WE
+ final int STEP = 8; // 4096 / 512 = 8 Vertices überspringen
+ final int SRC_VERTS = MapData.TERRAIN_VERTS; // 4097
+
+ float[] heights = new float[GAME_VERTS * GAME_VERTS];
+ for (int gz = 0; gz < GAME_VERTS; gz++) {
+ int sz = gz * STEP;
+ for (int gx = 0; gx < GAME_VERTS; gx++) {
+ heights[gz * GAME_VERTS + gx] = map.terrainHeight[sz * SRC_VERTS + gx * STEP];
+ }
+ }
+
+ // Höhe in der Weltmitte als Spawn-Grundlage
+ float centerHeight = heights[(GAME_VERTS / 2) * GAME_VERTS + (GAME_VERTS / 2)];
+ spawnY = centerHeight + 3f;
+
+ TerrainQuad terrain = new TerrainQuad("terrain", 65, GAME_VERTS, heights);
+ terrain.setLocalScale(8f, 1f, 8f); // 512 Zellen * 8 WE = 4096 WE pro Achse
+ terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
+
+ applyTerrainMaterial(terrain, 32f);
+
+ RigidBodyControl terrainPhysics = new RigidBodyControl(
+ CollisionShapeFactory.createMeshShape(terrain), 0f);
+ terrain.addControl(terrainPhysics);
+ bulletAppState.getPhysicsSpace().add(terrainPhysics);
+
+ rootNode.attachChild(terrain);
+ System.out.println("[WorldScene] Karte geladen, Spawn Y=" + spawnY);
+ return terrain;
+ }
+
+ /** Prozedurales Demo-Terrain als Fallback (keine gespeicherte Karte). */
+ private TerrainQuad buildProceduralTerrain() {
+ int size = 257;
+ float[] heights = new float[size * size];
+ for (int z = 0; z < size; z++) {
+ for (int x = 0; x < size; x++) {
+ float nx = x / (float) size;
+ float nz = z / (float) size;
+ heights[z * size + x] =
+ FastMath.sin(nx * FastMath.TWO_PI * 2) * 2f
+ + FastMath.sin(nz * FastMath.TWO_PI * 3) * 1.5f
+ + FastMath.sin((nx + nz) * FastMath.TWO_PI * 1.5f) * 1f;
+ }
+ }
+
+ spawnY = 5f;
+
+ TerrainQuad terrain = new TerrainQuad("terrain", 65, size, heights);
+ terrain.setLocalTranslation(0, -5f, 0);
+ terrain.setLocalScale(0.5f, 0.5f, 0.5f);
+ terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
+
+ applyTerrainMaterial(terrain, 64f);
+
+ 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");
+ try {
+ Texture grass = assetManager.loadTexture("Textures/gras.png");
+ grass.setWrap(Texture.WrapMode.Repeat);
+ mat.setTexture("Tex1", grass);
+ mat.setFloat("Tex1Scale", texScale);
+ } catch (Exception e) {
+ // Fallback: einfarbiges 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);
+ }
+
+ // -----------------------------------------------------------------------
+ // Dekorationen – Höhe per TerrainQuad.getHeight() anpassen
+ // -----------------------------------------------------------------------
+
+ private void buildDecorations(TerrainQuad terrain) {
+ Material stoneMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ stoneMat.setBoolean("UseMaterialColors", true);
+ stoneMat.setColor("Diffuse", new ColorRGBA(0.55f, 0.55f, 0.55f, 1f));
+ stoneMat.setColor("Ambient", new ColorRGBA(0.3f, 0.3f, 0.3f, 1f));
+ stoneMat.setColor("Specular", ColorRGBA.White);
+ stoneMat.setFloat("Shininess", 32f);
+
+ Material treeTrunkMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ treeTrunkMat.setBoolean("UseMaterialColors", true);
+ treeTrunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.28f, 0.1f, 1f));
+ treeTrunkMat.setColor("Ambient", new ColorRGBA(0.2f, 0.12f, 0.04f, 1f));
+ treeTrunkMat.setColor("Specular", ColorRGBA.Black);
+
+ Material treeTopMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ treeTopMat.setBoolean("UseMaterialColors", true);
+ treeTopMat.setColor("Diffuse", new ColorRGBA(0.1f, 0.55f, 0.15f, 1f));
+ treeTopMat.setColor("Ambient", new ColorRGBA(0.05f, 0.25f, 0.07f, 1f));
+ treeTopMat.setColor("Specular", ColorRGBA.Black);
+
+ float[][] treeXZ = {
+ {12, 8}, {-15, 5}, {20, -10}, {-8, -18},
+ {5, 25}, {-22, 12}, {18, 20}, {-10, -5}
+ };
+ for (float[] xz : treeXZ) {
+ float worldY = terrainWorldY(terrain, xz[0], xz[1]);
+ Node tree = new Node("tree");
+
+ Geometry trunk = new Geometry("trunk", new Cylinder(8, 8, 0.25f, 2.5f, true));
+ trunk.setMaterial(treeTrunkMat);
+ trunk.rotate(FastMath.HALF_PI, 0, 0);
+ trunk.setLocalTranslation(0, 1.25f, 0);
+ trunk.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+
+ Geometry crown = new Geometry("crown", new Sphere(12, 12, 2.2f));
+ crown.setMaterial(treeTopMat);
+ crown.setLocalTranslation(0, 3.8f, 0);
+ crown.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+
+ tree.attachChild(trunk);
+ tree.attachChild(crown);
+ tree.setLocalTranslation(xz[0], worldY, xz[1]);
+ rootNode.attachChild(tree);
+ }
+
+ float[][] stoneXZ = {{6, -6}, {-12, 15}, {16, -4}, {-3, 10}};
+ for (float[] xz : stoneXZ) {
+ float worldY = terrainWorldY(terrain, xz[0], xz[1]);
+ float r = 0.6f + FastMath.nextRandomFloat() * 0.8f;
+ Geometry stone = new Geometry("stone", new Sphere(8, 8, r));
+ stone.setMaterial(stoneMat);
+ stone.setLocalTranslation(xz[0], worldY + r * 0.5f, xz[1]);
+ stone.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ rootNode.attachChild(stone);
+ }
+ }
+
+ /** Konvertiert Welt-XZ in lokale Terrain-Koordinaten und fragt die Höhe ab. */
+ private float terrainWorldY(TerrainQuad terrain, float worldX, float worldZ) {
+ Vector3f terrainTranslation = terrain.getWorldTranslation();
+ Vector3f terrainScale = terrain.getWorldScale();
+ float localX = (worldX - terrainTranslation.x) / terrainScale.x;
+ float localZ = (worldZ - terrainTranslation.z) / terrainScale.z;
+ float localH = terrain.getHeight(new Vector2f(localX, localZ));
+ return terrainTranslation.y + localH * terrainScale.y;
+ }
+
+ // -----------------------------------------------------------------------
+ // Charakter (visueller Node, ohne eigenes Mesh für Physics)
+ // -----------------------------------------------------------------------
+
+ private Node buildCharacter() {
+ Node character = new Node("character");
+
+ Material bodyMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ bodyMat.setBoolean("UseMaterialColors", true);
+ bodyMat.setColor("Diffuse", new ColorRGBA(0.2f, 0.4f, 0.8f, 1f));
+ bodyMat.setColor("Ambient", new ColorRGBA(0.1f, 0.2f, 0.4f, 1f));
+ bodyMat.setColor("Specular", ColorRGBA.White);
+ bodyMat.setFloat("Shininess", 64f);
+
+ Material headMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
+ headMat.setBoolean("UseMaterialColors", true);
+ headMat.setColor("Diffuse", new ColorRGBA(0.9f, 0.75f, 0.6f, 1f));
+ headMat.setColor("Ambient", new ColorRGBA(0.45f, 0.37f, 0.3f, 1f));
+ headMat.setColor("Specular", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f));
+ headMat.setFloat("Shininess", 16f);
+
+ Geometry body = new Geometry("body", new Cylinder(8, 16, 0.35f, 1.1f, true));
+ body.setMaterial(bodyMat);
+ body.rotate(FastMath.HALF_PI, 0, 0);
+ body.setLocalTranslation(0, 0.9f, 0);
+ body.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+
+ Geometry head = new Geometry("head", new Sphere(16, 16, 0.28f));
+ head.setMaterial(headMat);
+ head.setLocalTranslation(0, 1.75f, 0);
+ head.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+
+ Geometry armL = buildLimb(bodyMat, 0.1f, 0.55f);
+ armL.setLocalTranslation(-0.5f, 0.85f, 0);
+ armL.rotate(0, 0, FastMath.DEG_TO_RAD * 20f);
+
+ Geometry armR = buildLimb(bodyMat, 0.1f, 0.55f);
+ armR.setLocalTranslation(0.5f, 0.85f, 0);
+ armR.rotate(0, 0, FastMath.DEG_TO_RAD * -20f);
+
+ Geometry legL = buildLimb(bodyMat, 0.12f, 0.6f);
+ legL.setLocalTranslation(-0.18f, 0.3f, 0);
+
+ Geometry legR = buildLimb(bodyMat, 0.12f, 0.6f);
+ legR.setLocalTranslation(0.18f, 0.3f, 0);
+
+ character.attachChild(body);
+ character.attachChild(head);
+ character.attachChild(armL);
+ character.attachChild(armR);
+ character.attachChild(legL);
+ character.attachChild(legR);
+
+ return character;
+ }
+
+ private Geometry buildLimb(Material mat, float radius, float height) {
+ Geometry limb = new Geometry("limb", new Cylinder(6, 12, radius, height, true));
+ limb.setMaterial(mat);
+ limb.rotate(FastMath.HALF_PI, 0, 0);
+ limb.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ return limb;
+ }
+}
diff --git a/blight-game/src/main/java/de/blight/game/state/GrassState.java b/blight-game/src/main/java/de/blight/game/state/GrassState.java
new file mode 100644
index 0000000..887aadc
--- /dev/null
+++ b/blight-game/src/main/java/de/blight/game/state/GrassState.java
@@ -0,0 +1,221 @@
+package de.blight.game.state;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.app.state.BaseAppState;
+import com.jme3.asset.AssetManager;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState;
+import com.jme3.math.*;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.terrain.geomipmap.TerrainQuad;
+import com.jme3.util.BufferUtils;
+import de.blight.common.MapData;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.*;
+
+/**
+ * Rendert Gras im Spiel aus der in MapData gespeicherten Dichte-Map.
+ *
+ * Chunks werden gestreckt über mehrere Frames aufgebaut (INIT_PER_FRAME),
+ * um Startlags zu vermeiden. GrassVisibilityControl cullt entfernte Chunks.
+ */
+public class GrassState extends BaseAppState {
+
+ // ── Konstanten (identisch mit PlacedObjectState im Editor) ────────────────
+ private static final int TERRAIN_HALF = 2048;
+ private static final float WORLD_SIZE = 4096f;
+ private static final int SPLAT_SIZE = MapData.SPLAT_SIZE;
+ private static final float SPLAT_WE_PER_PX = WORLD_SIZE / (SPLAT_SIZE - 1);
+ private static final int CHUNK_SIZE = 128;
+ private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE;
+ private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS;
+ private static final int MAX_BLADES_PER_PX = 3;
+ private static final float BLADE_WIDTH = 0.18f;
+ private static final float DEFAULT_HEIGHT = 1.5f;
+ private static final float FAR_DIST = 150f; // WE (game terrain is 1:1 WE)
+ private static final float FAR_DIST_SQ = FAR_DIST * FAR_DIST;
+ private static final int INIT_PER_FRAME = 4;
+
+ // ── Abhängigkeiten ────────────────────────────────────────────────────────
+ private final MapData mapData;
+ private final TerrainQuad terrain;
+
+ // ── Runtime-Zustand ───────────────────────────────────────────────────────
+ private Camera cam;
+ private Node grassNode;
+ private Material grassMat;
+ private int nextChunk = 0;
+
+ public GrassState(MapData mapData, TerrainQuad terrain) {
+ this.mapData = mapData;
+ this.terrain = terrain;
+ }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
+
+ @Override
+ protected void initialize(Application app) {
+ this.cam = app.getCamera();
+ grassNode = new Node("gameGrass");
+ ((SimpleApplication) app).getRootNode().attachChild(grassNode);
+ grassMat = buildGrassMaterial(app.getAssetManager());
+ }
+
+ @Override
+ protected void cleanup(Application app) {
+ ((SimpleApplication) app).getRootNode().detachChild(grassNode);
+ }
+
+ @Override protected void onEnable() { grassNode.setCullHint(Spatial.CullHint.Inherit); }
+ @Override protected void onDisable() { grassNode.setCullHint(Spatial.CullHint.Always); }
+
+ @Override
+ public void update(float tpf) {
+ int built = 0;
+ while (nextChunk < CHUNK_COUNT && built < INIT_PER_FRAME) {
+ buildChunk(nextChunk++);
+ built++;
+ }
+ }
+
+ // ── Material ──────────────────────────────────────────────────────────────
+
+ private Material buildGrassMaterial(AssetManager assets) {
+ try {
+ Material mat = new Material(assets, "MatDefs/Grass.j3md");
+ mat.setColor("Color", new ColorRGBA(0.28f, 0.72f, 0.18f, 1f));
+ mat.setFloat("WindSpeed", 0.5f);
+ mat.setFloat("WindStrength", 0.14f);
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ } catch (Exception e) {
+ System.err.println("[GrassState] Grass.j3md nicht gefunden, Fallback: " + e.getMessage());
+ Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
+ mat.setColor("Color", new ColorRGBA(0.25f, 0.65f, 0.15f, 1f));
+ mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
+ return mat;
+ }
+ }
+
+ // ── Chunk aufbauen ────────────────────────────────────────────────────────
+
+ private void buildChunk(int idx) {
+ int cx = idx % CHUNKS_PER_AXIS;
+ int cz = idx / CHUNKS_PER_AXIS;
+ float wXMin = -TERRAIN_HALF + cx * CHUNK_SIZE;
+ float wZMin = -TERRAIN_HALF + cz * CHUNK_SIZE;
+
+ int pxMin = Math.max(0, (int)((wXMin + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+ int pzMin = Math.max(0, (int)((wZMin + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+ int pxMax = Math.min(SPLAT_SIZE - 1, (int)((wXMin + CHUNK_SIZE + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+ int pzMax = Math.min(SPLAT_SIZE - 1, (int)((wZMin + CHUNK_SIZE + TERRAIN_HALF) / SPLAT_WE_PER_PX));
+
+ List blades = new ArrayList<>();
+ Vector3f scale = terrain.getWorldScale();
+ Vector3f trans = terrain.getWorldTranslation();
+
+ for (int pz = pzMin; pz <= pzMax; pz++) {
+ for (int px = pxMin; px <= pxMax; px++) {
+ int d = mapData.grassDensity[pz * SPLAT_SIZE + px] & 0xFF;
+ if (d == 0) continue;
+ int count = Math.max(1, (int)(d / 255f * MAX_BLADES_PER_PX));
+ Random rng = new Random((long) px * 100003L + pz);
+ float pixWorldX = px * SPLAT_WE_PER_PX - TERRAIN_HALF;
+ float pixWorldZ = pz * SPLAT_WE_PER_PX - TERRAIN_HALF;
+ for (int b = 0; b < count; b++) {
+ float bx = pixWorldX + (rng.nextFloat() - 0.5f) * SPLAT_WE_PER_PX;
+ float bz = pixWorldZ + (rng.nextFloat() - 0.5f) * SPLAT_WE_PER_PX;
+ // Welt→lokal→Höhe→Welt
+ float localX = (bx - trans.x) / scale.x;
+ float localZ = (bz - trans.z) / scale.z;
+ float th = terrain.getHeight(new Vector2f(localX, localZ));
+ if (Float.isNaN(th)) continue;
+ float worldY = trans.y + th * scale.y;
+ float h = DEFAULT_HEIGHT * (0.7f + rng.nextFloat() * 0.6f);
+ blades.add(new float[]{bx, worldY, bz, h});
+ }
+ }
+ }
+
+ if (blades.isEmpty()) return;
+
+ Mesh mesh = buildGrassMesh(blades);
+ float chunkCX = wXMin + CHUNK_SIZE * 0.5f;
+ float chunkCZ = wZMin + CHUNK_SIZE * 0.5f;
+ Geometry geo = new Geometry("grass_" + idx, mesh);
+ geo.setMaterial(grassMat);
+ geo.addControl(new GrassVisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ)));
+ grassNode.attachChild(geo);
+ }
+
+ // ── Mesh: Kreuz-Quad mit UV ───────────────────────────────────────────────
+
+ private static Mesh buildGrassMesh(List blades) {
+ int n = blades.size();
+ FloatBuffer pos = BufferUtils.createFloatBuffer(n * 8 * 3);
+ FloatBuffer uv = BufferUtils.createFloatBuffer(n * 8 * 2);
+ IntBuffer idx = BufferUtils.createIntBuffer(n * 12);
+
+ int vi = 0;
+ for (float[] blade : blades) {
+ float x = blade[0], y = blade[1], z = blade[2], h = blade[3];
+ float w = Math.max(0.05f, h * BLADE_WIDTH);
+
+ pos.put(x-w).put(y ).put(z); uv.put(0).put(0);
+ pos.put(x+w).put(y ).put(z); uv.put(1).put(0);
+ pos.put(x+w).put(y+h).put(z); uv.put(1).put(1);
+ pos.put(x-w).put(y+h).put(z); uv.put(0).put(1);
+
+ pos.put(x).put(y ).put(z-w); uv.put(0).put(0);
+ pos.put(x).put(y ).put(z+w); uv.put(1).put(0);
+ pos.put(x).put(y+h).put(z+w); uv.put(1).put(1);
+ pos.put(x).put(y+h).put(z-w); uv.put(0).put(1);
+
+ idx.put(vi ).put(vi+1).put(vi+2);
+ idx.put(vi ).put(vi+2).put(vi+3);
+ idx.put(vi+4).put(vi+5).put(vi+6);
+ idx.put(vi+4).put(vi+6).put(vi+7);
+ vi += 8;
+ }
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
+ mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv);
+ mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
+ mesh.updateBound();
+ return mesh;
+ }
+
+ // ── LOD-Control ───────────────────────────────────────────────────────────
+
+ private static final class GrassVisibilityControl extends AbstractControl {
+ private final Camera cam;
+ private final Vector3f center;
+
+ GrassVisibilityControl(Camera cam, Vector3f center) {
+ this.cam = cam;
+ this.center = center;
+ }
+
+ @Override
+ protected void controlUpdate(float tpf) {
+ float distSq = cam.getLocation().distanceSquared(center);
+ spatial.setCullHint(distSq > FAR_DIST_SQ
+ ? Spatial.CullHint.Always
+ : Spatial.CullHint.Inherit);
+ }
+
+ @Override protected void controlRender(RenderManager rm, ViewPort vp) {}
+ }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..efb7ad4
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,21 @@
+// ── Gemeinsame Konfiguration für alle Subprojekte ───────────────────────────
+
+allprojects {
+ group = 'de.blight'
+ version = '0.1.0'
+}
+
+subprojects {
+ apply plugin: 'java'
+
+ java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+
+ compileJava.options.encoding = 'UTF-8'
+
+ repositories {
+ mavenCentral()
+ }
+}
diff --git a/doc/Abschluss Kapitel 1.odt b/doc/Abschluss Kapitel 1.odt
new file mode 100644
index 0000000..8a660ff
Binary files /dev/null and b/doc/Abschluss Kapitel 1.odt differ
diff --git a/blight-data/doc/Option A.odt b/doc/Backlog/Option A.odt
similarity index 100%
rename from blight-data/doc/Option A.odt
rename to doc/Backlog/Option A.odt
diff --git a/blight-data/doc/Option B.odt b/doc/Backlog/Option B.odt
similarity index 100%
rename from blight-data/doc/Option B.odt
rename to doc/Backlog/Option B.odt
diff --git a/doc/Blight Crawler.odt b/doc/Blight Crawler.odt
new file mode 100644
index 0000000..c9f0840
Binary files /dev/null and b/doc/Blight Crawler.odt differ
diff --git a/doc/Eingeborene.odt b/doc/Eingeborene.odt
new file mode 100644
index 0000000..8551958
Binary files /dev/null and b/doc/Eingeborene.odt differ
diff --git a/doc/Klippenbeißer.odt b/doc/Klippenbeißer.odt
new file mode 100644
index 0000000..86db564
Binary files /dev/null and b/doc/Klippenbeißer.odt differ
diff --git a/blight-data/doc/Plott.odt b/doc/Plott.odt
similarity index 100%
rename from blight-data/doc/Plott.odt
rename to doc/Plott.odt
diff --git a/doc/Silas.odt b/doc/Silas.odt
new file mode 100644
index 0000000..8b0f4a8
Binary files /dev/null and b/doc/Silas.odt differ
diff --git a/doc/Skilltree.odt b/doc/Skilltree.odt
new file mode 100644
index 0000000..84157fe
Binary files /dev/null and b/doc/Skilltree.odt differ
diff --git a/ez-tree-jme/assets/MatDefs/Tree.j3md b/ez-tree-jme/assets/MatDefs/Tree.j3md
new file mode 100644
index 0000000..25e4729
--- /dev/null
+++ b/ez-tree-jme/assets/MatDefs/Tree.j3md
@@ -0,0 +1,21 @@
+MaterialDef Tree {
+
+ MaterialParameters {
+ Color Diffuse (Color) : 0.42 0.26 0.10 1.0
+ Float WindStrength : 0.15
+ Float WindSpeed : 0.5
+ Texture2D BarkMap
+ Boolean HasBarkMap : false
+ }
+
+ Technique {
+ VertexShader GLSL150 : Shaders/Tree.vert
+ FragmentShader GLSL150 : Shaders/Tree.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ Time
+ }
+ }
+}
diff --git a/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md b/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md
new file mode 100644
index 0000000..d57e4bd
--- /dev/null
+++ b/ez-tree-jme/assets/MatDefs/TreeLeaf.j3md
@@ -0,0 +1,83 @@
+MaterialDef TreeLeaf {
+
+ MaterialParameters {
+ Color Diffuse (Color) : 0.18 0.60 0.10 1.0
+ Float WindStrength : 0.30
+ Float WindSpeed : 0.7
+ Texture2D LeafMap
+ Boolean HasLeafMap : false
+
+ // Vom Shadow-Renderer befüllt (PostShadow-Pass) — vollständige Liste aus PostShadow.j3md
+ Int BoundDrawBuffer
+ Int FilterMode
+ Boolean HardwareShadows
+ Texture2D ShadowMap0
+ Texture2D ShadowMap1
+ Texture2D ShadowMap2
+ Texture2D ShadowMap3
+ Texture2D ShadowMap4
+ Texture2D ShadowMap5
+ Float ShadowIntensity : 1.0
+ Vector4 Splits
+ Vector2 FadeInfo
+ Matrix4 LightViewProjectionMatrix0
+ Matrix4 LightViewProjectionMatrix1
+ Matrix4 LightViewProjectionMatrix2
+ Matrix4 LightViewProjectionMatrix3
+ Matrix4 LightViewProjectionMatrix4
+ Matrix4 LightViewProjectionMatrix5
+ Vector3 LightPos
+ Vector3 LightDir
+ Float PCFEdge
+ Float ShadowMapSize
+ Boolean BackfaceShadows : false
+ }
+
+ Technique {
+ VertexShader GLSL150 : Shaders/Tree.vert
+ FragmentShader GLSL150 : Shaders/TreeLeaf.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ Time
+ }
+
+ RenderState {
+ FaceCull Off
+ }
+ }
+
+ Technique PostShadow {
+ VertexShader GLSL150 : Shaders/LeafPostShadow.vert
+ FragmentShader GLSL150 : Shaders/LeafPostShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ }
+
+ ForcedRenderState {
+ Blend Modulate
+ FaceCull Off
+ DepthWrite Off
+ }
+ }
+
+ Technique PreShadow {
+ VertexShader GLSL150 : Shaders/LeafPreShadow.vert
+ FragmentShader GLSL150 : Shaders/LeafPreShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+
+ ForcedRenderState {
+ FaceCull Off
+ DepthTest On
+ DepthWrite On
+ PolyOffset 5 3
+ ColorWrite Off
+ }
+ }
+}
diff --git a/ez-tree-jme/assets/Shaders/LeafPostShadow.frag b/ez-tree-jme/assets/Shaders/LeafPostShadow.frag
new file mode 100644
index 0000000..f87574b
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/LeafPostShadow.frag
@@ -0,0 +1,20 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform sampler2D m_ShadowMap0;
+uniform float m_ShadowIntensity;
+uniform sampler2D m_LeafMap;
+uniform bool m_HasLeafMap;
+
+in vec4 shadowCoord;
+in vec2 texCoord;
+
+void main() {
+ // Transparente Blattbereiche empfangen keinen Schatten
+ if (m_HasLeafMap && texture2D(m_LeafMap, texCoord).a < 0.5) discard;
+
+ vec3 coord = shadowCoord.xyz / shadowCoord.w;
+ float mapDepth = texture2D(m_ShadowMap0, coord.xy).r;
+ float lit = (coord.z > mapDepth + 0.001) ? (1.0 - m_ShadowIntensity) : 1.0;
+
+ gl_FragColor = vec4(lit, lit, lit, 1.0);
+}
diff --git a/ez-tree-jme/assets/Shaders/LeafPostShadow.vert b/ez-tree-jme/assets/Shaders/LeafPostShadow.vert
new file mode 100644
index 0000000..5b13bb8
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/LeafPostShadow.vert
@@ -0,0 +1,18 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldMatrix;
+uniform mat4 m_LightViewProjectionMatrix0;
+
+in vec3 inPosition;
+in vec2 inTexCoord;
+
+out vec4 shadowCoord;
+out vec2 texCoord;
+
+void main() {
+ gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
+ vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
+ shadowCoord = m_LightViewProjectionMatrix0 * worldPos;
+ texCoord = inTexCoord;
+}
diff --git a/ez-tree-jme/assets/Shaders/LeafPreShadow.frag b/ez-tree-jme/assets/Shaders/LeafPreShadow.frag
new file mode 100644
index 0000000..59a31e7
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/LeafPreShadow.frag
@@ -0,0 +1,15 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform sampler2D m_LeafMap;
+uniform bool m_HasLeafMap;
+
+in vec2 texCoord;
+
+void main() {
+ if (m_HasLeafMap) {
+ vec4 tex = texture2D(m_LeafMap, texCoord);
+ if (tex.a < 0.5) discard;
+ }
+ // Nur Tiefe schreiben — ColorWrite ist per ForcedRenderState deaktiviert
+ gl_FragColor = vec4(1.0);
+}
diff --git a/ez-tree-jme/assets/Shaders/LeafPreShadow.vert b/ez-tree-jme/assets/Shaders/LeafPreShadow.vert
new file mode 100644
index 0000000..8b60506
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/LeafPreShadow.vert
@@ -0,0 +1,13 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform mat4 g_WorldViewProjectionMatrix;
+
+in vec3 inPosition;
+in vec2 inTexCoord;
+
+out vec2 texCoord;
+
+void main() {
+ gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
+ texCoord = inTexCoord;
+}
diff --git a/ez-tree-jme/assets/Shaders/Tree.frag b/ez-tree-jme/assets/Shaders/Tree.frag
new file mode 100644
index 0000000..4232df7
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/Tree.frag
@@ -0,0 +1,29 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform vec4 m_Diffuse;
+uniform sampler2D m_BarkMap;
+uniform bool m_HasBarkMap;
+
+in vec2 texCoord;
+in vec3 worldNormal;
+
+void main() {
+ vec3 n = normalize(worldNormal);
+
+ // Sun at ~45° elevation from SE — good contrast on vertical cylinders
+ vec3 sunDir = normalize(vec3(0.6, 0.7, 0.4));
+ vec3 fillDir = normalize(vec3(-0.5, 0.3, -0.4));
+ vec3 rimDir = normalize(vec3(-0.3, 0.5, 0.7));
+
+ float sun = max(dot(n, sunDir), 0.0);
+ float fill = max(dot(n, fillDir), 0.0) * 0.22;
+ float rim = max(dot(n, rimDir), 0.0) * 0.14;
+ float sky = dot(n, vec3(0.0, 1.0, 0.0)) * 0.4 + 0.4; // [0.0, 0.8]
+ float light = sun * 0.75 + fill + rim + sky * 0.09 + 0.05;
+
+ vec3 baseColor = m_HasBarkMap
+ ? texture2D(m_BarkMap, texCoord).rgb * m_Diffuse.rgb
+ : m_Diffuse.rgb;
+
+ gl_FragColor = vec4(baseColor * clamp(light, 0.0, 1.0), m_Diffuse.a);
+}
diff --git a/ez-tree-jme/assets/Shaders/Tree.vert b/ez-tree-jme/assets/Shaders/Tree.vert
new file mode 100644
index 0000000..583272c
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/Tree.vert
@@ -0,0 +1,33 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldMatrix;
+uniform float g_Time;
+uniform float m_WindStrength;
+uniform float m_WindSpeed;
+
+in vec3 inPosition;
+in vec3 inNormal;
+in vec2 inTexCoord;
+in vec4 inColor; // R = Wind-Gewicht (0 = Wurzel, 1 = Spitze)
+
+out vec2 texCoord;
+out vec3 worldNormal;
+
+void main() {
+ float windW = inColor.r;
+ float t = g_Time * m_WindSpeed;
+
+ // Welt-Position für orts-abhängige Phase (verhindert synchrones Schwingen)
+ vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
+ float phase = worldPos.x * 0.08 + worldPos.z * 0.06;
+
+ float swayX = sin(t + phase) * windW * m_WindStrength;
+ float swayZ = cos(t * 0.73 + phase) * windW * m_WindStrength * 0.55;
+
+ vec3 animPos = inPosition + vec3(swayX, 0.0, swayZ);
+
+ gl_Position = g_WorldViewProjectionMatrix * vec4(animPos, 1.0);
+ texCoord = inTexCoord;
+ worldNormal = normalize((g_WorldMatrix * vec4(inNormal, 0.0)).xyz);
+}
diff --git a/ez-tree-jme/assets/Shaders/TreeLeaf.frag b/ez-tree-jme/assets/Shaders/TreeLeaf.frag
new file mode 100644
index 0000000..d805691
--- /dev/null
+++ b/ez-tree-jme/assets/Shaders/TreeLeaf.frag
@@ -0,0 +1,27 @@
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+
+uniform vec4 m_Diffuse;
+uniform sampler2D m_LeafMap;
+uniform bool m_HasLeafMap;
+
+in vec2 texCoord;
+in vec3 worldNormal;
+
+void main() {
+ vec3 baseColor;
+
+ if (m_HasLeafMap) {
+ vec4 tex = texture2D(m_LeafMap, texCoord);
+ if (tex.a < 0.5) discard;
+ baseColor = tex.rgb * m_Diffuse.rgb;
+ } else {
+ // Fallback: kreisförmiger Clip
+ vec2 uv = texCoord * 2.0 - 1.0;
+ if (dot(uv, uv) > 0.95) discard;
+ float edge = 1.0 - dot(uv, uv);
+ baseColor = m_Diffuse.rgb * (0.7 + 0.3 * edge);
+ }
+
+ // Leaves transmit light — no directional shading, uniform brightness
+ gl_FragColor = vec4(baseColor, 1.0);
+}
diff --git a/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg
new file mode 100644
index 0000000..6277b94
Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark001_Color.jpg differ
diff --git a/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg
new file mode 100644
index 0000000..b2525e9
Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark002_Color.jpg differ
diff --git a/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg
new file mode 100644
index 0000000..15ed21b
Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark003_Color.jpg differ
diff --git a/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg b/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg
new file mode 100644
index 0000000..43ee658
Binary files /dev/null and b/ez-tree-jme/assets/Textures/bark/Bark008_Color.jpg differ
diff --git a/ez-tree-jme/assets/Textures/leaves/ash.png b/ez-tree-jme/assets/Textures/leaves/ash.png
new file mode 100644
index 0000000..7f7c844
Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/ash.png differ
diff --git a/ez-tree-jme/assets/Textures/leaves/aspen.png b/ez-tree-jme/assets/Textures/leaves/aspen.png
new file mode 100644
index 0000000..89265a9
Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/aspen.png differ
diff --git a/ez-tree-jme/assets/Textures/leaves/oak.png b/ez-tree-jme/assets/Textures/leaves/oak.png
new file mode 100644
index 0000000..061fbf6
Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/oak.png differ
diff --git a/ez-tree-jme/assets/Textures/leaves/palm.png b/ez-tree-jme/assets/Textures/leaves/palm.png
new file mode 100644
index 0000000..4c8f4b2
Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/palm.png differ
diff --git a/ez-tree-jme/assets/Textures/leaves/pine.png b/ez-tree-jme/assets/Textures/leaves/pine.png
new file mode 100644
index 0000000..478dca3
Binary files /dev/null and b/ez-tree-jme/assets/Textures/leaves/pine.png differ
diff --git a/ez-tree-jme/build.gradle b/ez-tree-jme/build.gradle
new file mode 100644
index 0000000..e71d133
--- /dev/null
+++ b/ez-tree-jme/build.gradle
@@ -0,0 +1,15 @@
+ext {
+ jmeVersion = '3.9.0-stable'
+}
+
+dependencies {
+ implementation "org.jmonkeyengine:jme3-core:${jmeVersion}"
+}
+
+sourceSets {
+ main {
+ resources {
+ srcDirs = ['src/main/resources', 'assets']
+ }
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class
new file mode 100644
index 0000000..71c0561
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java
new file mode 100644
index 0000000..ca6cfcf
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/BarkOptions.java
@@ -0,0 +1,23 @@
+package de.blight.eztree;
+
+public final class BarkOptions {
+
+ /** Optional asset path for a bark diffuse texture. */
+ public String textureFile = null;
+ public boolean flatShading = false;
+ public float textureScaleX = 1f;
+ public float textureScaleY = 1f;
+
+ /** Base bark color (used when no texture is assigned). */
+ public float r = 0.45f, g = 0.30f, b = 0.18f;
+
+ public BarkOptions copy() {
+ BarkOptions c = new BarkOptions();
+ c.textureFile = textureFile;
+ c.flatShading = flatShading;
+ c.textureScaleX = textureScaleX;
+ c.textureScaleY = textureScaleY;
+ c.r = r; c.g = g; c.b = b;
+ return c;
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class
new file mode 100644
index 0000000..acb1ca3
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java
new file mode 100644
index 0000000..8ac7d1f
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/Billboard.java
@@ -0,0 +1,5 @@
+package de.blight.eztree;
+
+public enum Billboard {
+ NONE, CROSS, ROTATE_X
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java b/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java
new file mode 100644
index 0000000..8a8bee5
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/Branch.java
@@ -0,0 +1,18 @@
+package de.blight.eztree;
+
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+
+public final class Branch {
+
+ /** World-space start position. */
+ public Vector3f origin = new Vector3f();
+
+ /** Rotation that maps Vector3f.UNIT_Y to the initial branch direction. */
+ public Quaternion orientation = new Quaternion();
+
+ public float length;
+ public float radius;
+ public int level;
+ public Branch parent;
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class
new file mode 100644
index 0000000..2150f53
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java
new file mode 100644
index 0000000..8901d78
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/BranchOptions.java
@@ -0,0 +1,81 @@
+package de.blight.eztree;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * All branch parameters, stored as per-level maps (key = branch level).
+ * When a level is not present the highest defined level at or below is used.
+ */
+public final class BranchOptions {
+
+ /** Maximum branch depth (0 = trunk only). */
+ public int levels = 3;
+
+ /** Spread angle in degrees from parent direction for children at each level. */
+ public Map angle = new HashMap<>();
+ /** Number of child branches produced by a branch at each level. */
+ public Map children = new HashMap<>();
+ /** Random direction perturbation magnitude (radians) per section step. */
+ public Map gnarliness = new HashMap<>();
+ /** Gravitational / wind force applied to branch growth direction. */
+ public ForceOptions force = new ForceOptions();
+ /** Length multiplier for children (relative to their parent). Level 0 = absolute trunk length. */
+ public Map length = new HashMap<>();
+ /** Radius multiplier for children (relative to parent section radius). Level 0 = absolute trunk radius. */
+ public Map radius = new HashMap<>();
+ /** Number of cylinder sections along a branch at each level. */
+ public Map sections = new HashMap<>();
+ /** Number of polygon sides per cylinder cross-section at each level. */
+ public Map segments = new HashMap<>();
+ /** Fraction along parent at which children of this level begin to appear. */
+ public Map start = new HashMap<>();
+ /** Tip-radius / base-radius ratio for each level (0=needle, 1=no taper). */
+ public Map taper = new HashMap<>();
+ /** Degrees per section by which the ring frame is rotated around the branch axis. */
+ public Map twist = new HashMap<>();
+
+ // ── Accessors with fallback ──────────────────────────────────────────────
+
+ public float getAngle(int lv) { return floatAt(angle, lv, 45f); }
+ public int getChildren(int lv) { return intAt (children, lv, 0); }
+ public float getGnarliness(int lv){ return floatAt(gnarliness,lv, 0f); }
+ public float getLength(int lv) { return floatAt(length, lv, 1f); }
+ public float getRadius(int lv) { return floatAt(radius, lv, 0.5f);}
+ public int getSections(int lv) { return intAt (sections, lv, 4); }
+ public int getSegments(int lv) { return intAt (segments, lv, 6); }
+ public float getStart(int lv) { return floatAt(start, lv, 0f); }
+ public float getTaper(int lv) { return floatAt(taper, lv, 0.6f);}
+ public float getTwist(int lv) { return floatAt(twist, lv, 0f); }
+
+ private float floatAt(Map m, int lv, float def) {
+ if (m.containsKey(lv)) return m.get(lv);
+ int best = -1;
+ for (int k : m.keySet()) if (k <= lv && k > best) best = k;
+ return best >= 0 ? m.get(best) : def;
+ }
+
+ private int intAt(Map m, int lv, int def) {
+ if (m.containsKey(lv)) return m.get(lv);
+ int best = -1;
+ for (int k : m.keySet()) if (k <= lv && k > best) best = k;
+ return best >= 0 ? m.get(best) : def;
+ }
+
+ public BranchOptions copy() {
+ BranchOptions c = new BranchOptions();
+ c.levels = levels;
+ c.angle = new HashMap<>(angle);
+ c.children = new HashMap<>(children);
+ c.gnarliness = new HashMap<>(gnarliness);
+ c.force = force.copy();
+ c.length = new HashMap<>(length);
+ c.radius = new HashMap<>(radius);
+ c.sections = new HashMap<>(sections);
+ c.segments = new HashMap<>(segments);
+ c.start = new HashMap<>(start);
+ c.taper = new HashMap<>(taper);
+ c.twist = new HashMap<>(twist);
+ return c;
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java b/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java
new file mode 100644
index 0000000..3081f75
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/BranchSection.java
@@ -0,0 +1,6 @@
+package de.blight.eztree;
+
+import com.jme3.math.Vector3f;
+
+/** Immutable snapshot of one section along a branch. */
+record BranchSection(Vector3f pos, float radius, Vector3f dir, float t) {}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class
new file mode 100644
index 0000000..e428c35
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java
new file mode 100644
index 0000000..6807961
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/ForceOptions.java
@@ -0,0 +1,20 @@
+package de.blight.eztree;
+
+import com.jme3.math.Vector3f;
+
+public final class ForceOptions {
+
+ public Vector3f direction = new Vector3f(0f, 0.5f, 0f);
+ public float strength = 0.02f;
+
+ public ForceOptions() {}
+
+ public ForceOptions(float dx, float dy, float dz, float strength) {
+ this.direction.set(dx, dy, dz);
+ this.strength = strength;
+ }
+
+ public ForceOptions copy() {
+ return new ForceOptions(direction.x, direction.y, direction.z, strength);
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class
new file mode 100644
index 0000000..e294fbc
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java
new file mode 100644
index 0000000..3549b63
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/LeavesOptions.java
@@ -0,0 +1,30 @@
+package de.blight.eztree;
+
+public final class LeavesOptions {
+
+ /** Optional asset path for a leaf alpha texture. */
+ public String textureFile = null;
+ public Billboard billboard = Billboard.CROSS;
+ public int count = 10;
+ /** Fraction along the last-level branch at which leaf placement begins. */
+ public float start = 0.4f;
+ public float size = 0.5f;
+ public float sizeVariance = 0.25f;
+ public float alphaTest = 0.5f;
+
+ /** Base leaf color. */
+ public float r = 0.13f, g = 0.53f, b = 0.17f;
+
+ public LeavesOptions copy() {
+ LeavesOptions c = new LeavesOptions();
+ c.textureFile = textureFile;
+ c.billboard = billboard;
+ c.count = count;
+ c.start = start;
+ c.size = size;
+ c.sizeVariance = sizeVariance;
+ c.alphaTest = alphaTest;
+ c.r = r; c.g = g; c.b = b;
+ return c;
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java b/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java
new file mode 100644
index 0000000..0b51320
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/Rng.java
@@ -0,0 +1,30 @@
+package de.blight.eztree;
+
+/** Marsaglia's Multiply-With-Carry RNG — matches the ez-tree JS implementation. */
+public final class Rng {
+
+ private int mz = 987654321;
+ private int mw;
+
+ public Rng(int seed) {
+ this.mw = (seed == 0) ? 12345 : seed;
+ }
+
+ /** Returns a value in [0, 1). */
+ public float next() {
+ mz = 36969 * (mz & 0xFFFF) + (mz >>> 16);
+ mw = 18000 * (mw & 0xFFFF) + (mw >>> 16);
+ long result = (((long) mz << 16) + (mw & 0xFFFFL)) & 0xFFFFFFFFL;
+ return (float) (result / 4294967296.0);
+ }
+
+ /** Returns a value in [min, max). */
+ public float range(float min, float max) {
+ return min + next() * (max - min);
+ }
+
+ /** Returns an integer in [min, max). */
+ public int integer(int min, int max) {
+ return min + (int) (next() * (max - min));
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java b/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java
new file mode 100644
index 0000000..3a8e55a
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/Tree.java
@@ -0,0 +1,398 @@
+package de.blight.eztree;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.BufferUtils;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * JME3 port of the ez-tree procedural tree generator.
+ *
+ * After construction call {@link #generate()} (or re-call it to regenerate).
+ * The resulting scene graph has two child {@link Geometry} nodes:
+ *
+ * - {@code "bark"} — cylinder mesh with VertexBuffer.Color.R = wind factor
+ * - {@code "leaves"} — quad mesh with VertexBuffer.Color.R = wind factor
+ *
+ * Materials must be assigned by the caller.
+ */
+public class Tree extends Node {
+
+ private final TreeOptions opts;
+
+ // Accumulated geometry data, reset on each generate() call
+ private final List barkPos = new ArrayList<>();
+ private final List barkNorm = new ArrayList<>();
+ private final List barkUV = new ArrayList<>();
+ private final List barkIdx = new ArrayList<>();
+ private final List barkWind = new ArrayList<>();
+
+ private final List leafPos = new ArrayList<>();
+ private final List leafNorm = new ArrayList<>();
+ private final List leafUV = new ArrayList<>();
+ private final List leafIdx = new ArrayList<>();
+ private final List leafWind = new ArrayList<>();
+
+ public Tree(TreeOptions opts) {
+ super("EzTree");
+ this.opts = opts;
+ }
+
+ public TreeOptions getOptions() { return opts; }
+
+ // ── Public API ───────────────────────────────────────────────────────────
+
+ /** Clears previous geometry and rebuilds the tree mesh. */
+ public void generate() {
+ detachAllChildren();
+ barkPos.clear(); barkNorm.clear(); barkUV.clear(); barkIdx.clear(); barkWind.clear();
+ leafPos.clear(); leafNorm.clear(); leafUV.clear(); leafIdx.clear(); leafWind.clear();
+
+ Rng rng = new Rng(opts.seed);
+
+ Branch trunk = new Branch();
+ trunk.level = 0;
+ trunk.length = opts.branch.getLength(0);
+ trunk.radius = opts.branch.getRadius(0);
+
+ Queue queue = new ArrayDeque<>();
+ queue.add(trunk);
+
+ while (!queue.isEmpty()) {
+ processBranch(queue.poll(), rng, queue);
+ }
+
+ if (!barkPos.isEmpty()) {
+ attachChild(buildGeometry("bark", barkPos, barkNorm, barkUV, barkIdx, barkWind));
+ }
+ if (!leafPos.isEmpty()) {
+ attachChild(buildGeometry("leaves", leafPos, leafNorm, leafUV, leafIdx, leafWind));
+ }
+
+ if (opts.trellis.enabled) {
+ Node trellisNode = Trellis.build(opts.trellis);
+ attachChild(trellisNode);
+ }
+ }
+
+ // ── Branch processing ────────────────────────────────────────────────────
+
+ private void processBranch(Branch branch, Rng rng, Queue queue) {
+ BranchOptions bo = opts.branch;
+ int level = branch.level;
+ int sections = bo.getSections(level);
+ int segments = bo.getSegments(level);
+ float gnarliness = bo.getGnarliness(level);
+ float taper = bo.getTaper(level);
+ float twistStep = bo.getTwist(level) * FastMath.DEG_TO_RAD;
+ float sectionLen = branch.length / sections;
+
+ float startRad = branch.radius;
+ float endRad = startRad * taper;
+
+ // Initial direction = orientation applied to Y+
+ Vector3f dir = branch.orientation.mult(Vector3f.UNIT_Y).normalizeLocal();
+ Vector3f pos = branch.origin.clone();
+
+ // Parallel-transport frame for smooth cylinder rings
+ Vector3f frameRight = findPerp(dir);
+ Vector3f frameUp = dir.cross(frameRight).normalizeLocal();
+
+ List pts = new ArrayList<>(sections + 1);
+ pts.add(new BranchSection(pos.clone(), startRad, dir.clone(), 0f));
+
+ for (int i = 0; i < sections; i++) {
+ float t = (i + 1f) / sections;
+
+ // Gnarliness: random XZ perturbation of direction
+ if (gnarliness > 0f) {
+ dir.x += rng.range(-gnarliness, gnarliness);
+ dir.z += rng.range(-gnarliness, gnarliness);
+ dir.normalizeLocal();
+ }
+
+ // External force (gravity / wind bias)
+ Vector3f fd = bo.force.direction;
+ float fs = bo.force.strength;
+ if (fs != 0f) {
+ dir.x += fd.x * fs;
+ dir.y += fd.y * fs;
+ dir.z += fd.z * fs;
+ dir.normalizeLocal();
+ }
+
+ // Twist: spin the transport frame around the branch axis
+ if (twistStep != 0f) {
+ Quaternion twistQ = new Quaternion().fromAngleAxis(twistStep, dir);
+ frameRight = twistQ.mult(frameRight).normalizeLocal();
+ frameUp = dir.cross(frameRight).normalizeLocal();
+ }
+
+ pos.addLocal(dir.mult(sectionLen));
+ float radius = startRad + (endRad - startRad) * t;
+ pts.add(new BranchSection(pos.clone(), radius, dir.clone(), t));
+ }
+
+ addCylinderGeometry(pts, segments, taper);
+
+ if (level < bo.levels) {
+ generateChildren(branch, pts, rng, queue);
+ }
+
+ // Leaves on the last (finest) level branches
+ if (level == bo.levels) {
+ generateLeaves(pts, rng);
+ }
+ }
+
+ // ── Cylinder mesh contribution ───────────────────────────────────────────
+
+ private void addCylinderGeometry(List pts, int segments, float taper) {
+ int vertsPerRing = segments + 1;
+ int baseVertex = barkPos.size() / 3;
+
+ // Parallel-transport frame
+ Vector3f frameRight = findPerp(pts.get(0).dir());
+ Vector3f frameUp = pts.get(0).dir().cross(frameRight).normalizeLocal();
+
+ for (BranchSection sp : pts) {
+ Vector3f d = sp.dir().normalize();
+
+ // Transport the frame to the new direction
+ float dot = frameRight.dot(d);
+ frameRight = frameRight.subtract(d.mult(dot));
+ if (frameRight.lengthSquared() < 1e-6f) {
+ frameRight = findPerp(d);
+ } else {
+ frameRight.normalizeLocal();
+ }
+ frameUp = d.cross(frameRight).normalizeLocal();
+
+ float uv_v = (1f - sp.t()) * opts.bark.textureScaleY;
+
+ for (int seg = 0; seg <= segments; seg++) {
+ float angle = (float) seg / segments * FastMath.TWO_PI;
+ float cosA = FastMath.cos(angle);
+ float sinA = FastMath.sin(angle);
+
+ // Vertex = center + radial offset along (right, up)
+ float ox = frameRight.x * cosA + frameUp.x * sinA;
+ float oy = frameRight.y * cosA + frameUp.y * sinA;
+ float oz = frameRight.z * cosA + frameUp.z * sinA;
+ float r = sp.radius();
+
+ barkPos.add(sp.pos().x + ox * r);
+ barkPos.add(sp.pos().y + oy * r);
+ barkPos.add(sp.pos().z + oz * r);
+
+ // Normal = outward radial direction
+ float nlen = FastMath.sqrt(ox * ox + oy * oy + oz * oz);
+ barkNorm.add(ox / nlen);
+ barkNorm.add(oy / nlen);
+ barkNorm.add(oz / nlen);
+
+ barkUV.add((float) seg / segments * opts.bark.textureScaleX);
+ barkUV.add(uv_v);
+ barkWind.add(sp.t());
+ }
+ }
+
+ // Connect adjacent rings with quads
+ int numRings = pts.size();
+ for (int si = 0; si < numRings - 1; si++) {
+ int r0 = baseVertex + si * vertsPerRing;
+ int r1 = baseVertex + (si + 1) * vertsPerRing;
+ for (int seg = 0; seg < segments; seg++) {
+ int a = r0 + seg, b = r0 + seg + 1;
+ int c = r1 + seg, d = r1 + seg + 1;
+ barkIdx.add(a); barkIdx.add(b); barkIdx.add(c);
+ barkIdx.add(b); barkIdx.add(d); barkIdx.add(c);
+ }
+ }
+ }
+
+ // ── Child branch placement ───────────────────────────────────────────────
+
+ private void generateChildren(Branch parent, List pts,
+ Rng rng, Queue queue) {
+ BranchOptions bo = opts.branch;
+ int level = parent.level;
+ int childLevel = level + 1;
+ int childCount = bo.getChildren(level);
+ float childStart = bo.getStart(childLevel);
+ float childLenFactor = bo.getLength(childLevel);
+ float childRadFactor = bo.getRadius(childLevel);
+ float childAngle = bo.getAngle(childLevel) * FastMath.DEG_TO_RAD;
+ float childTwistBase = bo.getTwist(childLevel) * FastMath.DEG_TO_RAD;
+ int sections = pts.size() - 1;
+
+ for (int i = 0; i < childCount; i++) {
+ // Stratified placement along parent with small jitter
+ float t = childStart + ((float) i / childCount) * (1f - childStart);
+ t = FastMath.clamp(t + rng.range(-0.02f, 0.02f), childStart, 1f);
+
+ int sectionIdx = Math.min(Math.max(0, (int) (t * sections)), sections);
+ BranchSection sp = pts.get(sectionIdx);
+
+ // Azimuth: evenly distribute around parent axis, with optional twist accumulation
+ float phi = ((float) i / childCount) * FastMath.TWO_PI
+ + childTwistBase * i
+ + rng.range(-0.15f, 0.15f);
+
+ Vector3f parentDir = sp.dir().normalize();
+ Vector3f perp = findPerp(parentDir);
+
+ // Rotate perp around parentDir by phi to obtain the swing axis
+ Quaternion phiQ = new Quaternion().fromAngleAxis(phi, parentDir);
+ Vector3f swingAxis = phiQ.mult(perp).normalizeLocal();
+
+ // Rotate parentDir by childAngle around swingAxis
+ Quaternion thetaQ = new Quaternion().fromAngleAxis(childAngle, swingAxis);
+ Vector3f childDir = thetaQ.mult(parentDir).normalizeLocal();
+
+ Branch child = new Branch();
+ child.origin = sp.pos().clone();
+ child.orientation = dirToQuat(childDir);
+ child.level = childLevel;
+ child.parent = parent;
+ child.length = parent.length * childLenFactor;
+ child.radius = sp.radius() * childRadFactor;
+
+ queue.add(child);
+ }
+ }
+
+ // ── Leaf placement ───────────────────────────────────────────────────────
+
+ private void generateLeaves(List pts, Rng rng) {
+ LeavesOptions lo = opts.leaves;
+ int count = lo.count;
+ int nSections = pts.size() - 1;
+
+ for (int i = 0; i < count; i++) {
+ float t = lo.start + rng.next() * (1f - lo.start);
+ int si = Math.min(Math.max(0, (int) (t * nSections)), nSections);
+ BranchSection sp = pts.get(si);
+
+ float size = lo.size * (1f + rng.range(-lo.sizeVariance, lo.sizeVariance));
+ float yaw = rng.range(0f, FastMath.TWO_PI);
+ float pitch = rng.range(-FastMath.QUARTER_PI * 0.4f, FastMath.QUARTER_PI * 0.4f);
+ float wind = sp.t();
+
+ switch (lo.billboard) {
+ case CROSS -> {
+ addLeafQuad(sp.pos(), yaw, pitch, size, wind);
+ addLeafQuad(sp.pos(), yaw + FastMath.HALF_PI, pitch, size, wind);
+ }
+ case ROTATE_X -> addLeafQuad(sp.pos(), yaw, FastMath.HALF_PI, size, wind);
+ default -> addLeafQuad(sp.pos(), yaw, pitch, size, wind);
+ }
+ }
+ }
+
+ private void addLeafQuad(Vector3f center, float yaw, float pitch, float size, float wind) {
+ float h = size * 0.5f;
+ float cy = FastMath.cos(yaw), sy = FastMath.sin(yaw);
+ float cp = FastMath.cos(pitch), sp = FastMath.sin(pitch);
+
+ // Local frame: right = rotate X by yaw, up = rotated Y by yaw+pitch
+ Vector3f right = new Vector3f(cy, 0f, -sy);
+ Vector3f up = new Vector3f(sy * sp, cp, cy * sp);
+ Vector3f norm = right.cross(up).normalizeLocal();
+
+ // 4 corners: BL, BR, TR, TL
+ float[] cornerX = { center.x - right.x*h - up.x*h,
+ center.x + right.x*h - up.x*h,
+ center.x + right.x*h + up.x*h,
+ center.x - right.x*h + up.x*h };
+ float[] cornerY = { center.y - right.y*h - up.y*h,
+ center.y + right.y*h - up.y*h,
+ center.y + right.y*h + up.y*h,
+ center.y - right.y*h + up.y*h };
+ float[] cornerZ = { center.z - right.z*h - up.z*h,
+ center.z + right.z*h - up.z*h,
+ center.z + right.z*h + up.z*h,
+ center.z - right.z*h + up.z*h };
+
+ int base = leafPos.size() / 3;
+ for (int i = 0; i < 4; i++) {
+ leafPos.add(cornerX[i]); leafPos.add(cornerY[i]); leafPos.add(cornerZ[i]);
+ leafNorm.add(norm.x); leafNorm.add(norm.y); leafNorm.add(norm.z);
+ leafWind.add(wind);
+ }
+ leafUV.add(0f); leafUV.add(0f);
+ leafUV.add(1f); leafUV.add(0f);
+ leafUV.add(1f); leafUV.add(1f);
+ leafUV.add(0f); leafUV.add(1f);
+
+ leafIdx.add(base); leafIdx.add(base+1); leafIdx.add(base+2);
+ leafIdx.add(base); leafIdx.add(base+2); leafIdx.add(base+3);
+ }
+
+ // ── Mesh assembly ────────────────────────────────────────────────────────
+
+ private static Geometry buildGeometry(String name,
+ List pos, List norm,
+ List uv, List idx,
+ List wind) {
+ int nVerts = pos.size() / 3;
+
+ FloatBuffer pb = BufferUtils.createFloatBuffer(nVerts * 3);
+ FloatBuffer nb = BufferUtils.createFloatBuffer(nVerts * 3);
+ FloatBuffer ub = BufferUtils.createFloatBuffer(nVerts * 2);
+ FloatBuffer cb = BufferUtils.createFloatBuffer(nVerts * 4); // RGBA — R = wind
+ IntBuffer ib = BufferUtils.createIntBuffer(idx.size());
+
+ for (float f : pos) pb.put(f);
+ for (float f : norm) nb.put(f);
+ for (float f : uv) ub.put(f);
+ for (float f : wind) { cb.put(f); cb.put(1f); cb.put(1f); cb.put(1f); }
+ for (int i : idx) ib.put(i);
+
+ pb.flip(); nb.flip(); ub.flip(); cb.flip(); ib.flip();
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(Type.Position, 3, pb);
+ mesh.setBuffer(Type.Normal, 3, nb);
+ mesh.setBuffer(Type.TexCoord, 2, ub);
+ mesh.setBuffer(Type.Color, 4, cb);
+ mesh.setBuffer(Type.Index, 3, ib);
+ mesh.updateBound();
+
+ return new Geometry(name, mesh);
+ }
+
+ // ── Math helpers ─────────────────────────────────────────────────────────
+
+ /** Returns a unit vector perpendicular to v. */
+ private static Vector3f findPerp(Vector3f v) {
+ Vector3f p = v.cross(Vector3f.UNIT_X);
+ if (p.lengthSquared() < 0.01f) p = v.cross(Vector3f.UNIT_Z);
+ return p.normalizeLocal();
+ }
+
+ /** Creates a quaternion that rotates UNIT_Y onto dir. */
+ private static Quaternion dirToQuat(Vector3f dir) {
+ Vector3f d = dir.normalize();
+ Vector3f axis = Vector3f.UNIT_Y.cross(d);
+ float len = axis.length();
+ if (len < 1e-6f) {
+ if (Vector3f.UNIT_Y.dot(d) > 0f) return new Quaternion();
+ return new Quaternion().fromAngleAxis(FastMath.PI, Vector3f.UNIT_X);
+ }
+ axis.divideLocal(len);
+ float angle = FastMath.acos(FastMath.clamp(Vector3f.UNIT_Y.dot(d), -1f, 1f));
+ return new Quaternion().fromAngleAxis(angle, axis);
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class
new file mode 100644
index 0000000..4366ac8
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java
new file mode 100644
index 0000000..12f061a
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/TreeOptions.java
@@ -0,0 +1,22 @@
+package de.blight.eztree;
+
+public final class TreeOptions {
+
+ public int seed = 0;
+ public TreeType type = TreeType.DECIDUOUS;
+ public BarkOptions bark = new BarkOptions();
+ public BranchOptions branch = new BranchOptions();
+ public LeavesOptions leaves = new LeavesOptions();
+ public TrellisOptions trellis = new TrellisOptions();
+
+ public TreeOptions copy() {
+ TreeOptions c = new TreeOptions();
+ c.seed = seed;
+ c.type = type;
+ c.bark = bark.copy();
+ c.branch = branch.copy();
+ c.leaves = leaves.copy();
+ c.trellis = trellis.copy();
+ return c;
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class
new file mode 100644
index 0000000..c1d6178
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java
new file mode 100644
index 0000000..1b5f596
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/TreePresets.java
@@ -0,0 +1,537 @@
+package de.blight.eztree;
+
+/**
+ * Presets from https://github.com/dgreenheck/ez-tree/tree/main/src/lib/presets
+ *
+ * Scaling applied vs. raw JSON values to match JME3 meter units:
+ * trunk length[0] ÷ 4
+ * trunk radius[0] ÷ 4
+ * leaf size ÷ 5
+ * Relative factors (length/radius level 1+) are JSON[n]/JSON[n-1] — unchanged.
+ * Twist: JSON radians × 57.2958 → degrees.
+ * Pine children capped at 20-22 (JS uses 91-100 for needle density, our generator
+ * creates full branch geometry per child).
+ */
+public final class TreePresets {
+
+ private TreePresets() {}
+
+ private static final String BARK1 = "Textures/bark/Bark001_Color.jpg";
+ private static final String BARK2 = "Textures/bark/Bark002_Color.jpg";
+ private static final String BARK3 = "Textures/bark/Bark003_Color.jpg";
+
+ private static final String LEAF_OAK = "Textures/leaves/oak.png";
+ private static final String LEAF_ASH = "Textures/leaves/ash.png";
+ private static final String LEAF_ASPEN = "Textures/leaves/aspen.png";
+ private static final String LEAF_PINE = "Textures/leaves/pine.png";
+
+ // ════════════════════════════════════════════════════════════════════════
+ // OAK
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions oakSmall() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 30895;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 54f); b.angle.put(2, 58f); b.angle.put(3, 32f);
+ b.children.put(0, 4); b.children.put(1, 2); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
+ b.gnarliness.put(0, 0.07f); b.gnarliness.put(1, 0.08f);
+ b.gnarliness.put(2, 0.11f); b.gnarliness.put(3, 0.09f);
+ b.length.put(0, 7.02f); b.length.put(1, 0.162f); b.length.put(2, 2.149f); b.length.put(3, 0.732f);
+ b.radius.put(0, 0.25f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 16); b.sections.put(1, 9); b.sections.put(2, 8);
+ b.segments.put(0, 7); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3);
+ b.start.put(1, 0.49f); b.start.put(2, 0.06f); b.start.put(3, 0.12f);
+ b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f);
+ b.twist.put(0, -13.18f); b.twist.put(1, 24.06f);
+
+ o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 14, 0.16f, 0.28f, 0.70f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions oakMedium() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 35729;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 54f); b.angle.put(2, 58f); b.angle.put(3, 32f);
+ b.children.put(0, 6); b.children.put(1, 4); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.02f;
+ b.gnarliness.put(0, 0.00f); b.gnarliness.put(1, 0.10f);
+ b.gnarliness.put(2, 0.15f); b.gnarliness.put(3, 0.09f);
+ b.length.put(0, 9.31f); b.length.put(1, 0.298f); b.length.put(2, 1.118f); b.length.put(3, 0.578f);
+ b.radius.put(0, 0.35f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 8); b.sections.put(1, 6); b.sections.put(2, 4);
+ b.segments.put(0, 7); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3);
+ b.start.put(1, 0.49f); b.start.put(2, 0.06f); b.start.put(3, 0.12f);
+ b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f);
+ b.twist.put(0, -13.18f); b.twist.put(1, 24.06f);
+
+ o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 18, 0.16f, 0.50f, 0.70f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions oakLarge() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 23399;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 1.000f; o.bark.g = 0.953f; o.bark.b = 0.820f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 10f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 54f); b.angle.put(2, 43f); b.angle.put(3, 32f);
+ b.children.put(0, 9); b.children.put(1, 5); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.02f;
+ b.gnarliness.put(0, 0.04f); b.gnarliness.put(1, 0.16f);
+ b.gnarliness.put(2, 0.06f); b.gnarliness.put(3, 0.09f);
+ b.length.put(0, 11.93f); b.length.put(1, 0.616f); b.length.put(2, 0.600f); b.length.put(3, 0.406f);
+ b.radius.put(0, 0.75f); b.radius.put(1, 0.58f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 16); b.sections.put(1, 9); b.sections.put(2, 8); b.sections.put(3, 3);
+ b.segments.put(0, 12); b.segments.put(1, 5); b.segments.put(2, 3); b.segments.put(3, 3);
+ b.start.put(1, 0.35f); b.start.put(2, 0.10f); b.start.put(3, 0.00f);
+ b.taper.put(0, 0.73f); b.taper.put(1, 0.42f); b.taper.put(2, 0.69f); b.taper.put(3, 0.75f);
+ b.twist.put(0, -13.18f); b.twist.put(1, 24.06f);
+
+ o.leaves = leaves(LEAF_OAK, 0.835f, 0.835f, 0.804f, Billboard.CROSS, 10, 0.16f, 0.90f, 0.70f, 0.5f);
+ return o;
+ }
+
+ // ════════════════════════════════════════════════════════════════════════
+ // ASH
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions ashSmall() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 26867;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
+ o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
+
+ BranchOptions b = o.branch;
+ b.levels = 2;
+ b.angle.put(1, 48f); b.angle.put(2, 75f); b.angle.put(3, 60f);
+ b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
+ b.gnarliness.put(0, 0.11f); b.gnarliness.put(1, 0.09f);
+ b.gnarliness.put(2, 0.05f); b.gnarliness.put(3, 0.09f);
+ b.length.put(0, 5.97f); b.length.put(1, 0.754f); b.length.put(2, 0.311f); b.length.put(3, 0.823f);
+ b.radius.put(0, 0.20f); b.radius.put(1, 0.62f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 10); b.sections.put(3, 10);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.53f); b.start.put(2, 0.33f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+ b.twist.put(0, 17.19f); b.twist.put(1, -4.01f);
+
+ o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 30, 0.00f, 0.41f, 0.717f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions ashMedium() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 36330;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
+ o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 48f); b.angle.put(2, 75f); b.angle.put(3, 60f);
+ b.children.put(0, 7); b.children.put(1, 4); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
+ b.gnarliness.put(0, 0.03f); b.gnarliness.put(1, 0.25f);
+ b.gnarliness.put(2, 0.20f); b.gnarliness.put(3, 0.09f);
+ b.length.put(0, 10.87f); b.length.put(1, 0.624f); b.length.put(2, 0.350f); b.length.put(3, 0.484f);
+ b.radius.put(0, 0.50f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 8); b.sections.put(2, 6); b.sections.put(3, 4);
+ b.segments.put(0, 12); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.23f); b.start.put(2, 0.33f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+ b.twist.put(0, 5.16f); b.twist.put(1, -4.01f);
+
+ o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 16, 0.00f, 0.53f, 0.720f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions ashLarge() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 29919;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
+ o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 39f); b.angle.put(2, 39f); b.angle.put(3, 51f);
+ b.children.put(0, 10); b.children.put(1, 4); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.01f;
+ b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.20f);
+ b.gnarliness.put(2, 0.16f); b.gnarliness.put(3, 0.05f);
+ b.length.put(0, 11.25f); b.length.put(1, 0.654f); b.length.put(2, 0.520f); b.length.put(3, 0.301f);
+ b.radius.put(0, 0.76f); b.radius.put(1, 0.58f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 8); b.sections.put(2, 6); b.sections.put(3, 4);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.32f); b.start.put(2, 0.34f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.62f); b.taper.put(2, 0.76f); b.taper.put(3, 0.00f);
+ b.twist.put(0, 5.16f); b.twist.put(1, -4.01f);
+
+ o.leaves = leaves(LEAF_ASH, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 10, 0.01f, 0.92f, 0.720f, 0.5f);
+ return o;
+ }
+
+ // ════════════════════════════════════════════════════════════════════════
+ // ASPEN
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions aspenSmall() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 36330;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK2;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
+
+ BranchOptions b = o.branch;
+ b.levels = 2;
+ b.angle.put(1, 70f); b.angle.put(2, 35f); b.angle.put(3, 7f);
+ b.children.put(0, 4); b.children.put(1, 3); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.011f;
+ b.gnarliness.put(0, 0.04f); b.gnarliness.put(1, 0.01f);
+ b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f);
+ b.length.put(0, 6.00f); b.length.put(1, 0.140f); b.length.put(2, 2.292f); b.length.put(3, 0.130f);
+ b.radius.put(0, 0.093f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.45f); b.start.put(2, 0.33f);
+ b.taper.put(0, 0.37f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+
+ o.leaves = leaves(LEAF_ASPEN, 1.000f, 0.980f, 0.384f, Billboard.CROSS, 13, 0.20f, 0.50f, 0.70f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions aspenMedium() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 18020;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK2;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
+
+ BranchOptions b = o.branch;
+ b.levels = 2;
+ b.angle.put(1, 75f); b.angle.put(2, 32f); b.angle.put(3, 7f);
+ b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.015f;
+ b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.12f);
+ b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f);
+ b.length.put(0, 12.50f); b.length.put(1, 0.121f); b.length.put(2, 1.843f); b.length.put(3, 0.089f);
+ b.radius.put(0, 0.18f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.59f); b.start.put(2, 0.35f);
+ b.taper.put(0, 0.37f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+
+ o.leaves = leaves(LEAF_ASPEN, 1.000f, 0.980f, 0.384f, Billboard.CROSS, 11, 0.124f, 0.50f, 0.70f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions aspenLarge() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 30631;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK2;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
+
+ BranchOptions b = o.branch;
+ b.levels = 2;
+ b.angle.put(1, 47f); b.angle.put(2, 63f); b.angle.put(3, 7f);
+ b.children.put(0, 10); b.children.put(1, 6); b.children.put(2, 0);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.022f;
+ b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.03f);
+ b.gnarliness.put(2, 0.12f); b.gnarliness.put(3, 0.02f);
+ b.length.put(0, 17.40f); b.length.put(1, 0.267f); b.length.put(2, 0.603f); b.length.put(3, 0.089f);
+ b.radius.put(0, 0.28f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8); b.sections.put(3, 6);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.62f); b.start.put(2, 0.05f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.13f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+
+ o.leaves = leaves(LEAF_ASPEN, 0.988f, 1.000f, 0.149f, Billboard.CROSS, 20, 0.152f, 0.70f, 0.70f, 0.5f);
+ return o;
+ }
+
+ // ════════════════════════════════════════════════════════════════════════
+ // PINE
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions pineSmall() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 11744;
+ o.type = TreeType.EVERGREEN;
+ o.bark.textureFile = BARK3;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
+
+ BranchOptions b = o.branch;
+ b.levels = 1;
+ b.angle.put(1, 117f); b.angle.put(2, 60f);
+ b.children.put(0, 20); b.children.put(1, 7);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
+ b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f);
+ b.length.put(0, 9.89f); b.length.put(1, 0.307f); b.length.put(2, 0.825f);
+ b.radius.put(0, 0.14f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4);
+ b.start.put(1, 0.16f); b.start.put(2, 0.30f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f);
+
+ o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 21, 0.00f, 0.19f, 0.70f, 0.3f);
+ return o;
+ }
+
+ public static TreeOptions pineMedium() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 13977;
+ o.type = TreeType.EVERGREEN;
+ o.bark.textureFile = BARK3;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
+
+ BranchOptions b = o.branch;
+ b.levels = 1;
+ b.angle.put(1, 110f); b.angle.put(2, 16f);
+ b.children.put(0, 18); b.children.put(1, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = -0.003f;
+ b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f);
+ b.length.put(0, 12.50f); b.length.put(1, 0.477f); b.length.put(2, 0.590f);
+ b.radius.put(0, 0.26f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4);
+ b.start.put(1, 0.27f); b.start.put(2, 0.14f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f);
+
+ o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 30, 0.09f, 0.29f, 0.201f, 0.3f);
+ return o;
+ }
+
+ public static TreeOptions pineLarge() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 44166;
+ o.type = TreeType.EVERGREEN;
+ o.bark.textureFile = BARK3;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 1f;
+
+ BranchOptions b = o.branch;
+ b.levels = 1;
+ b.angle.put(1, 129f); b.angle.put(2, 16f);
+ b.children.put(0, 22); b.children.put(1, 3);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.009f;
+ b.gnarliness.put(0, 0.05f); b.gnarliness.put(1, 0.08f);
+ b.length.put(0, 16.31f); b.length.put(1, 0.534f); b.length.put(2, 0.782f);
+ b.radius.put(0, 0.32f); b.radius.put(1, 0.55f); b.radius.put(2, 0.50f);
+ b.sections.put(0, 12); b.sections.put(1, 10); b.sections.put(2, 8);
+ b.segments.put(0, 8); b.segments.put(1, 6); b.segments.put(2, 4);
+ b.start.put(1, 0.294f); b.start.put(2, 0.14f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f);
+
+ o.leaves = leaves(LEAF_PINE, 1.0f, 1.0f, 1.0f, Billboard.CROSS, 18, 0.076f, 0.52f, 0.201f, 0.3f);
+ return o;
+ }
+
+ // ════════════════════════════════════════════════════════════════════════
+ // BUSH
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions bush1() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 45590;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
+ o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 21.52f); b.angle.put(2, 62.61f); b.angle.put(3, 60f);
+ b.children.put(0, 7); b.children.put(1, 3); b.children.put(2, 2);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
+ b.gnarliness.put(0, 0.11f); b.gnarliness.put(1, 0.09f);
+ b.gnarliness.put(2, 0.05f); b.gnarliness.put(3, 0.09f);
+ // Near-zero trunk: 0.25f stump, factor adjusted so actual branch1 ≈ 3.8 units
+ b.length.put(0, 0.25f); b.length.put(1, 15.30f); b.length.put(2, 0.365f); b.length.put(3, 0.823f);
+ b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 4); b.sections.put(1, 6); b.sections.put(2, 10); b.sections.put(3, 10);
+ b.segments.put(0, 4); b.segments.put(1, 4); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.53f); b.start.put(2, 0.33f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+ b.twist.put(0, 17.19f); b.twist.put(1, -4.01f);
+
+ o.leaves = leaves(LEAF_ASH, 0.878f, 1.000f, 0.835f, Billboard.CROSS, 12, 0.00f, 0.49f, 0.717f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions bush2() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 45590;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
+ o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
+
+ BranchOptions b = o.branch;
+ b.levels = 2;
+ b.angle.put(1, 19.57f); b.angle.put(2, 27.39f); b.angle.put(3, 60f);
+ b.children.put(0, 10); b.children.put(1, 3); b.children.put(2, 2);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
+ b.gnarliness.put(0, 0.022f); b.gnarliness.put(1, 0.109f);
+ b.gnarliness.put(2, 0.050f); b.gnarliness.put(3, 0.090f);
+ // Actual branch1 ≈ 4.9 units (19.646/4 ÷ 0.25)
+ b.length.put(0, 0.25f); b.length.put(1, 19.65f); b.length.put(2, 0.392f); b.length.put(3, 0.597f);
+ b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 3); b.sections.put(1, 4); b.sections.put(2, 10); b.sections.put(3, 10);
+ b.segments.put(0, 4); b.segments.put(1, 4); b.segments.put(2, 4); b.segments.put(3, 3);
+ b.start.put(1, 0.641f); b.start.put(2, 0.707f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+ b.twist.put(0, 20.55f); b.twist.put(1, -2.49f);
+
+ o.leaves = leaves(LEAF_ASPEN, 0.878f, 1.000f, 0.835f, Billboard.CROSS, 7, 0.00f, 0.49f, 0.717f, 0.5f);
+ return o;
+ }
+
+ public static TreeOptions bush3() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 31343;
+ o.type = TreeType.EVERGREEN;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 0.808f; o.bark.g = 0.800f; o.bark.b = 0.745f;
+ o.bark.textureScaleX = 0.5f; o.bark.textureScaleY = 5f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 66.52f); b.angle.put(2, 52.83f); b.angle.put(3, 0f);
+ b.children.put(0, 13); b.children.put(1, 4); b.children.put(2, 4);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0f;
+ b.gnarliness.put(0, 0.054f); b.gnarliness.put(1, 0.065f);
+ b.gnarliness.put(2, 0.050f); b.gnarliness.put(3, 0.090f);
+ b.length.put(0, 2.74f); b.length.put(1, 1.991f); b.length.put(2, 0.602f); b.length.put(3, 0.421f);
+ b.radius.put(0, 0.14f); b.radius.put(1, 0.65f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 4); b.sections.put(1, 3); b.sections.put(2, 3); b.sections.put(3, 10);
+ b.segments.put(0, 3); b.segments.put(1, 3); b.segments.put(2, 3); b.segments.put(3, 3);
+ b.start.put(1, 0.141f); b.start.put(2, 0.294f);
+ b.taper.put(0, 0.70f); b.taper.put(1, 0.70f); b.taper.put(2, 0.70f); b.taper.put(3, 0.70f);
+ b.twist.put(0, 17.19f); b.twist.put(1, -1.87f);
+
+ o.leaves = leaves(LEAF_PINE, 0.616f, 0.765f, 1.000f, Billboard.CROSS, 3, 0.152f, 0.61f, 0.457f, 0.5f);
+ return o;
+ }
+
+ // ════════════════════════════════════════════════════════════════════════
+ // TRELLIS
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions trellis() {
+ TreeOptions o = new TreeOptions();
+ o.seed = 41563;
+ o.type = TreeType.DECIDUOUS;
+ o.bark.textureFile = BARK1;
+ o.bark.r = 1.0f; o.bark.g = 1.0f; o.bark.b = 1.0f;
+ o.bark.textureScaleX = 1f; o.bark.textureScaleY = 8f;
+
+ BranchOptions b = o.branch;
+ b.levels = 3;
+ b.angle.put(1, 26f); b.angle.put(2, 79f); b.angle.put(3, 0f);
+ b.children.put(0, 7); b.children.put(1, 5); b.children.put(2, 1);
+ b.force.direction.set(0f, 1f, 0f); b.force.strength = 0.026f;
+ b.gnarliness.put(0, 0.00f); b.gnarliness.put(1, 0.02f);
+ b.gnarliness.put(2, 0.41f); b.gnarliness.put(3, 0.09f);
+ b.length.put(0, 1.20f); b.length.put(1, 3.521f); b.length.put(2, 0.669f); b.length.put(3, 0.982f);
+ b.radius.put(0, 0.068f); b.radius.put(1, 0.60f); b.radius.put(2, 0.55f); b.radius.put(3, 0.50f);
+ b.sections.put(0, 6); b.sections.put(1, 12); b.sections.put(2, 10); b.sections.put(3, 4);
+ b.segments.put(0, 3); b.segments.put(1, 3); b.segments.put(2, 3); b.segments.put(3, 3);
+ b.start.put(1, 0.19f); b.start.put(2, 0.10f); b.start.put(3, 0.06f);
+ b.taper.put(0, 0.60f); b.taper.put(1, 0.50f); b.taper.put(2, 0.50f); b.taper.put(3, 0.50f);
+ b.twist.put(0, -1.15f); b.twist.put(1, -0.57f); b.twist.put(2, 5.16f);
+
+ // tint 15204310 = 0xE7D8C6: (231,216,198)/255
+ o.leaves = leaves(LEAF_ASH, 0.906f, 0.847f, 0.776f, Billboard.NONE, 13, 0.00f, 0.34f, 0.50f, 0.5f);
+
+ o.trellis.enabled = true;
+ o.trellis.sections = 8;
+ o.trellis.radius = 0.5f;
+ o.trellis.length = 4f;
+ o.trellis.memberRadius = 0.08f;
+ o.trellis.crossMembers = 4;
+
+ return o;
+ }
+
+ // ════════════════════════════════════════════════════════════════════════
+ // Lookup
+ // ════════════════════════════════════════════════════════════════════════
+
+ public static TreeOptions byName(String name) {
+ return switch (name.toLowerCase()) {
+ case "oak small" -> oakSmall();
+ case "oak medium" -> oakMedium();
+ case "oak large" -> oakLarge();
+ case "ash small" -> ashSmall();
+ case "ash medium" -> ashMedium();
+ case "ash large" -> ashLarge();
+ case "aspen small" -> aspenSmall();
+ case "aspen medium" -> aspenMedium();
+ case "aspen large" -> aspenLarge();
+ case "pine small" -> pineSmall();
+ case "pine medium" -> pineMedium();
+ case "pine large" -> pineLarge();
+ case "bush 1" -> bush1();
+ case "bush 2" -> bush2();
+ case "bush 3" -> bush3();
+ case "trellis" -> trellis();
+ default -> oakMedium();
+ };
+ }
+
+ public static String[] presetNames() {
+ return new String[]{
+ "Oak Small", "Oak Medium", "Oak Large",
+ "Ash Small", "Ash Medium", "Ash Large",
+ "Aspen Small", "Aspen Medium", "Aspen Large",
+ "Pine Small", "Pine Medium", "Pine Large",
+ "Bush 1", "Bush 2", "Bush 3",
+ "Trellis"
+ };
+ }
+
+ private static LeavesOptions leaves(String tex, float r, float g, float b,
+ Billboard bb, int count,
+ float start, float size, float sizeVar, float alpha) {
+ LeavesOptions l = new LeavesOptions();
+ l.textureFile = tex;
+ l.r = r; l.g = g; l.b = b;
+ l.billboard = bb;
+ l.count = count;
+ l.start = start;
+ l.size = size;
+ l.sizeVariance = sizeVar;
+ l.alphaTest = alpha;
+ return l;
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class
new file mode 100644
index 0000000..d8d1811
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java
new file mode 100644
index 0000000..68b57c2
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/TreeType.java
@@ -0,0 +1,5 @@
+package de.blight.eztree;
+
+public enum TreeType {
+ DECIDUOUS, EVERGREEN
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java b/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java
new file mode 100644
index 0000000..0cdf19a
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/Trellis.java
@@ -0,0 +1,141 @@
+package de.blight.eztree;
+
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.util.BufferUtils;
+
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Builds a cylindrical lattice (trellis) for climbing plants.
+ *
+ * The trellis consists of {@code sections} vertical poles arranged in a circle
+ * of {@code radius}, connected by {@code crossMembers} horizontal rings.
+ * All members are thin cylinders with {@code memberRadius}.
+ */
+public final class Trellis {
+
+ private Trellis() {}
+
+ public static Node build(TrellisOptions o) {
+ Node node = new Node("trellis");
+
+ int poles = Math.max(3, o.sections);
+ int crossMembers = Math.max(1, o.crossMembers);
+ float totalHeight = o.length;
+ float poleRadius = o.radius;
+ float memberR = o.memberRadius;
+ int cylSegs = 5;
+
+ List pos = new ArrayList<>();
+ List norm = new ArrayList<>();
+ List uv = new ArrayList<>();
+ List idx = new ArrayList<>();
+
+ // Vertical poles
+ for (int p = 0; p < poles; p++) {
+ float phi = (float) p / poles * FastMath.TWO_PI;
+ float px = FastMath.cos(phi) * poleRadius;
+ float pz = FastMath.sin(phi) * poleRadius;
+ addCylinder(pos, norm, uv, idx,
+ new Vector3f(px, 0f, pz),
+ new Vector3f(px, totalHeight, pz),
+ memberR, cylSegs);
+ }
+
+ // Horizontal cross-member rings
+ for (int cm = 0; cm < crossMembers; cm++) {
+ float y = totalHeight * (cm + 1f) / (crossMembers + 1f);
+ for (int p = 0; p < poles; p++) {
+ float phi0 = (float) p / poles * FastMath.TWO_PI;
+ float phi1 = (float) (p + 1) / poles * FastMath.TWO_PI;
+ Vector3f from = new Vector3f(
+ FastMath.cos(phi0) * poleRadius, y, FastMath.sin(phi0) * poleRadius);
+ Vector3f to = new Vector3f(
+ FastMath.cos(phi1) * poleRadius, y, FastMath.sin(phi1) * poleRadius);
+ addCylinder(pos, norm, uv, idx, from, to, memberR, cylSegs);
+ }
+ }
+
+ Geometry geom = toGeometry("trellis-mesh", pos, norm, uv, idx);
+ node.attachChild(geom);
+ return node;
+ }
+
+ // ── Cylinder helper ──────────────────────────────────────────────────────
+
+ private static void addCylinder(List pos, List norm, List uv,
+ List idx,
+ Vector3f from, Vector3f to, float radius, int segs) {
+ Vector3f dir = to.subtract(from).normalizeLocal();
+ Vector3f right = findPerp(dir);
+ Vector3f up = dir.cross(right).normalizeLocal();
+
+ int base = pos.size() / 3;
+
+ for (int ring = 0; ring <= 1; ring++) {
+ Vector3f center = (ring == 0) ? from : to;
+ float v = ring;
+ for (int s = 0; s <= segs; s++) {
+ float a = (float) s / segs * FastMath.TWO_PI;
+ float ca = FastMath.cos(a), sa = FastMath.sin(a);
+ float ox = right.x * ca + up.x * sa;
+ float oy = right.y * ca + up.y * sa;
+ float oz = right.z * ca + up.z * sa;
+ pos.add(center.x + ox * radius);
+ pos.add(center.y + oy * radius);
+ pos.add(center.z + oz * radius);
+ float nl = FastMath.sqrt(ox*ox + oy*oy + oz*oz);
+ norm.add(ox / nl); norm.add(oy / nl); norm.add(oz / nl);
+ uv.add((float) s / segs); uv.add(v);
+ }
+ }
+
+ int ringSz = segs + 1;
+ for (int s = 0; s < segs; s++) {
+ int a = base + s, b = base + s + 1;
+ int c = base + ringSz + s, d = base + ringSz + s + 1;
+ idx.add(a); idx.add(b); idx.add(c);
+ idx.add(b); idx.add(d); idx.add(c);
+ }
+ }
+
+ private static Geometry toGeometry(String name,
+ List pos, List norm,
+ List uv, List idx) {
+ int nv = pos.size() / 3;
+ FloatBuffer pb = BufferUtils.createFloatBuffer(nv * 3);
+ FloatBuffer nb = BufferUtils.createFloatBuffer(nv * 3);
+ FloatBuffer ub = BufferUtils.createFloatBuffer(nv * 2);
+ IntBuffer ib = BufferUtils.createIntBuffer(idx.size());
+
+ for (float f : pos) pb.put(f);
+ for (float f : norm) nb.put(f);
+ for (float f : uv) ub.put(f);
+ for (int i : idx) ib.put(i);
+
+ pb.flip(); nb.flip(); ub.flip(); ib.flip();
+
+ Mesh mesh = new Mesh();
+ mesh.setBuffer(Type.Position, 3, pb);
+ mesh.setBuffer(Type.Normal, 3, nb);
+ mesh.setBuffer(Type.TexCoord, 2, ub);
+ mesh.setBuffer(Type.Index, 3, ib);
+ mesh.updateBound();
+
+ return new Geometry(name, mesh);
+ }
+
+ private static Vector3f findPerp(Vector3f v) {
+ Vector3f p = v.cross(Vector3f.UNIT_X);
+ if (p.lengthSquared() < 0.01f) p = v.cross(Vector3f.UNIT_Z);
+ return p.normalizeLocal();
+ }
+}
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class
new file mode 100644
index 0000000..1c91f9d
Binary files /dev/null and b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.class differ
diff --git a/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java
new file mode 100644
index 0000000..693daf5
--- /dev/null
+++ b/ez-tree-jme/src/main/java/de/blight/eztree/TrellisOptions.java
@@ -0,0 +1,24 @@
+package de.blight.eztree;
+
+public final class TrellisOptions {
+
+ public boolean enabled = false;
+ public int sections = 4;
+ public float radius = 0.5f;
+ public float length = 2f;
+ /** Radius of the cylindrical trellis members. */
+ public float memberRadius = 0.04f;
+ /** Number of cross-members per section. */
+ public int crossMembers = 3;
+
+ public TrellisOptions copy() {
+ TrellisOptions c = new TrellisOptions();
+ c.enabled = enabled;
+ c.sections = sections;
+ c.radius = radius;
+ c.length = length;
+ c.memberRadius = memberRadius;
+ c.crossMembers = crossMembers;
+ return c;
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e644113
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a034286
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..97de990
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/img/editor/grasstool.png b/img/editor/grasstool.png
new file mode 100644
index 0000000..7f581fe
Binary files /dev/null and b/img/editor/grasstool.png differ
diff --git a/img/editor/terraintool.png b/img/editor/terraintool.png
new file mode 100644
index 0000000..f33bc63
Binary files /dev/null and b/img/editor/terraintool.png differ
diff --git a/img/editor/terraintool_plateau.png b/img/editor/terraintool_plateau.png
new file mode 100644
index 0000000..f3f8e1c
Binary files /dev/null and b/img/editor/terraintool_plateau.png differ
diff --git a/img/editor/terraintool_sinus.png b/img/editor/terraintool_sinus.png
new file mode 100644
index 0000000..bb8dfc6
Binary files /dev/null and b/img/editor/terraintool_sinus.png differ
diff --git a/img/editor/terraintool_smooth.png b/img/editor/terraintool_smooth.png
new file mode 100644
index 0000000..2eff4bd
Binary files /dev/null and b/img/editor/terraintool_smooth.png differ
diff --git a/img/editor/terraintool_spike.png b/img/editor/terraintool_spike.png
new file mode 100644
index 0000000..88f6c67
Binary files /dev/null and b/img/editor/terraintool_spike.png differ
diff --git a/img/editor/textruretool.png b/img/editor/textruretool.png
new file mode 100644
index 0000000..11e4518
Binary files /dev/null and b/img/editor/textruretool.png differ
diff --git a/blight-data/logo/logo.png b/logo/logo.png
similarity index 100%
rename from blight-data/logo/logo.png
rename to logo/logo.png
diff --git a/blight-data/logo/orig.png b/logo/orig.png
similarity index 100%
rename from blight-data/logo/orig.png
rename to logo/orig.png
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..b214c1b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,8 @@
+rootProject.name = 'blight'
+
+include 'blight-common'
+include 'blight-assets'
+include 'blight-editor'
+include 'blight-game'
+include 'simarboreal'
+include 'ez-tree-jme'
diff --git a/simarboreal/.gitignore b/simarboreal/.gitignore
new file mode 100644
index 0000000..da88288
--- /dev/null
+++ b/simarboreal/.gitignore
@@ -0,0 +1 @@
+/.gradle/
diff --git a/simarboreal/assets/MatDefs/MultiResolution.frag b/simarboreal/assets/MatDefs/MultiResolution.frag
new file mode 100644
index 0000000..9682797
--- /dev/null
+++ b/simarboreal/assets/MatDefs/MultiResolution.frag
@@ -0,0 +1,332 @@
+#import "Common/ShaderLib/Parallax.glsllib"
+#import "Common/ShaderLib/Optics.glsllib"
+#define ATTENUATION
+//#define HQ_ATTENUATION
+
+#import "MatDefs/FragScattering.glsllib"
+
+varying vec2 texCoord;
+#ifdef SEPARATE_TEXCOORD
+ varying vec2 texCoord2;
+#endif
+
+varying vec3 AmbientSum;
+varying vec4 DiffuseSum;
+varying vec3 SpecularSum;
+
+varying float z;
+
+#ifndef VERTEX_LIGHTING
+ uniform vec4 g_LightDirection;
+ //varying vec3 vPosition;
+ varying vec3 vViewDir;
+ varying vec4 vLightDir;
+ varying vec3 lightVec;
+#else
+ varying vec2 vertexLightValues;
+#endif
+
+#ifdef DIFFUSEMAP
+ uniform sampler2D m_DiffuseMap;
+ uniform sampler2D m_BackgroundDiffuseMap;
+ uniform sampler2D m_NoiseMap;
+#endif
+
+#ifdef SPECULARMAP
+ uniform sampler2D m_SpecularMap;
+#endif
+
+#ifdef PARALLAXMAP
+ uniform sampler2D m_ParallaxMap;
+#endif
+#if (defined(PARALLAXMAP) || (defined(NORMALMAP_PARALLAX) && defined(NORMALMAP))) && !defined(VERTEX_LIGHTING)
+ uniform float m_ParallaxHeight;
+#endif
+
+#ifdef LIGHTMAP
+ uniform sampler2D m_LightMap;
+#endif
+
+#ifdef NORMALMAP
+ uniform sampler2D m_NormalMap;
+#else
+ varying vec3 vNormal;
+#endif
+
+#ifdef ALPHAMAP
+ uniform sampler2D m_AlphaMap;
+#endif
+
+#ifdef COLORRAMP
+ uniform sampler2D m_ColorRamp;
+#endif
+
+uniform float m_AlphaDiscardThreshold;
+
+#ifndef VERTEX_LIGHTING
+uniform float m_Shininess;
+
+#ifdef HQ_ATTENUATION
+uniform vec4 g_LightPosition;
+#endif
+
+#ifdef USE_REFLECTION
+ uniform float m_ReflectionPower;
+ uniform float m_ReflectionIntensity;
+ varying vec4 refVec;
+
+ uniform ENVMAP m_EnvMap;
+#endif
+
+float tangDot(in vec3 v1, in vec3 v2){
+ float d = dot(v1,v2);
+ #ifdef V_TANGENT
+ d = 1.0 - d*d;
+ return step(0.0, d) * sqrt(d);
+ #else
+ return d;
+ #endif
+}
+
+float lightComputeDiffuse(in vec3 norm, in vec3 lightdir, in vec3 viewdir){
+ #ifdef MINNAERT
+ float NdotL = max(0.0, dot(norm, lightdir));
+ float NdotV = max(0.0, dot(norm, viewdir));
+ return NdotL * pow(max(NdotL * NdotV, 0.1), -1.0) * 0.5;
+ #else
+ return max(0.0, dot(norm, lightdir));
+ #endif
+}
+
+float lightComputeSpecular(in vec3 norm, in vec3 viewdir, in vec3 lightdir, in float shiny){
+ // NOTE: check for shiny <= 1 removed since shininess is now
+ // 1.0 by default (uses matdefs default vals)
+ #ifdef LOW_QUALITY
+ // Blinn-Phong
+ // Note: preferably, H should be computed in the vertex shader
+ vec3 H = (viewdir + lightdir) * vec3(0.5);
+ return pow(max(tangDot(H, norm), 0.0), shiny);
+ #elif defined(WARDISO)
+ // Isotropic Ward
+ vec3 halfVec = normalize(viewdir + lightdir);
+ float NdotH = max(0.001, tangDot(norm, halfVec));
+ float NdotV = max(0.001, tangDot(norm, viewdir));
+ float NdotL = max(0.001, tangDot(norm, lightdir));
+ float a = tan(acos(NdotH));
+ float p = max(shiny/128.0, 0.001);
+ return NdotL * (1.0 / (4.0*3.14159265*p*p)) * (exp(-(a*a)/(p*p)) / (sqrt(NdotV * NdotL)));
+ #else
+ // Standard Phong
+ vec3 R = reflect(-lightdir, norm);
+ return pow(max(tangDot(R, viewdir), 0.0), shiny);
+ #endif
+}
+
+vec2 computeLighting(in vec3 wvNorm, in vec3 wvViewDir, in vec3 wvLightDir){
+ float diffuseFactor = lightComputeDiffuse(wvNorm, wvLightDir, wvViewDir);
+ float specularFactor = lightComputeSpecular(wvNorm, wvViewDir, wvLightDir, m_Shininess);
+
+ #ifdef HQ_ATTENUATION
+ float att = clamp(1.0 - g_LightPosition.w * length(lightVec), 0.0, 1.0);
+ #else
+ float att = vLightDir.w;
+ #endif
+
+ if (m_Shininess <= 1.0) {
+ specularFactor = 0.0; // should be one instruction on most cards ..
+ }
+
+ specularFactor *= diffuseFactor;
+
+ return vec2(diffuseFactor, specularFactor) * vec2(att);
+}
+#endif
+
+vec4 getColor( in sampler2D diffuseMap, in sampler2D diffuseMap2,
+ in sampler2D normalMap, in vec2 tc, in float distMix,
+ out vec3 normal ) {
+
+ vec2 tcOffset;
+ tcOffset = texture2D(m_NoiseMap, tc * 0.01).xy * 6.0 - 3.0;
+ vec4 diffuseColor = texture2D(diffuseMap, (tc + tcOffset) * 0.75);
+
+ tcOffset = (texture2D(m_NoiseMap, tc * 0.01).xy * 6.0) - 3.0;
+ vec4 subColor = texture2D(diffuseMap2, ((tc + tcOffset) * 1.0) * 0.1 );
+ diffuseColor = mix(diffuseColor, subColor, distMix);
+
+ #ifdef NORMALMAP
+ vec4 normalHeight = texture2D(normalMap, tc);
+ normal = normalize((normalHeight.xyz * vec3(2.0) - vec3(1.0)));
+ #else
+ normal = vec3(0.0, 1.0, 0.0);
+ #endif
+
+ return diffuseColor;
+}
+
+
+void main(){
+ vec2 newTexCoord;
+
+ #if (defined(PARALLAXMAP) || (defined(NORMALMAP_PARALLAX) && defined(NORMALMAP))) && !defined(VERTEX_LIGHTING)
+
+ #ifdef STEEP_PARALLAX
+ #ifdef NORMALMAP_PARALLAX
+ //parallax map is stored in the alpha channel of the normal map
+ newTexCoord = steepParallaxOffset(m_NormalMap, vViewDir, texCoord, m_ParallaxHeight);
+ #else
+ //parallax map is a texture
+ newTexCoord = steepParallaxOffset(m_ParallaxMap, vViewDir, texCoord, m_ParallaxHeight);
+ #endif
+ #else
+ #ifdef NORMALMAP_PARALLAX
+ //parallax map is stored in the alpha channel of the normal map
+ newTexCoord = classicParallaxOffset(m_NormalMap, vViewDir, texCoord, m_ParallaxHeight);
+ #else
+ //parallax map is a texture
+ newTexCoord = classicParallaxOffset(m_ParallaxMap, vViewDir, texCoord, m_ParallaxHeight);
+ #endif
+ #endif
+ #else
+ newTexCoord = texCoord;
+ #endif
+
+ float distMix = z / 32.0;
+ distMix = clamp(distMix, 0.4, 1.0);
+
+ #ifdef DIFFUSEMAP
+ vec3 newNormal;
+ #ifdef NORMALMAP
+ vec4 diffuseColor = getColor(m_DiffuseMap, m_BackgroundDiffuseMap,
+ m_NormalMap, texCoord, distMix, newNormal);
+ #else
+ vec4 diffuseColor = getColor(m_DiffuseMap, m_BackgroundDiffuseMap,
+ m_DiffuseMap, texCoord, distMix, newNormal);
+ #endif
+ #else
+ vec4 diffuseColor = vec4(1.0);
+ vec3 newNormal = vec3(0.0, 1.0, 0.0);
+ #endif
+
+ float alpha = DiffuseSum.a * diffuseColor.a;
+ #ifdef ALPHAMAP
+ alpha = alpha * texture2D(m_AlphaMap, newTexCoord).r;
+ #endif
+ if(alpha < m_AlphaDiscardThreshold){
+ discard;
+ }
+
+ #ifndef VERTEX_LIGHTING
+ float spotFallOff = 1.0;
+
+ #if __VERSION__ >= 110
+ // allow use of control flow
+ if(g_LightDirection.w != 0.0){
+ #endif
+
+ vec3 L = normalize(lightVec.xyz);
+ vec3 spotdir = normalize(g_LightDirection.xyz);
+ float curAngleCos = dot(-L, spotdir);
+ float innerAngleCos = floor(g_LightDirection.w) * 0.001;
+ float outerAngleCos = fract(g_LightDirection.w);
+ float innerMinusOuter = innerAngleCos - outerAngleCos;
+ spotFallOff = (curAngleCos - outerAngleCos) / innerMinusOuter;
+
+ #if __VERSION__ >= 110
+ if(spotFallOff <= 0.0){
+ gl_FragColor.rgb = AmbientSum * diffuseColor.rgb;
+ gl_FragColor.a = alpha;
+ return;
+ }else{
+ spotFallOff = clamp(spotFallOff, 0.0, 1.0);
+ }
+ }
+ #else
+ spotFallOff = clamp(spotFallOff, step(g_LightDirection.w, 0.001), 1.0);
+ #endif
+ #endif
+
+ // ***********************
+ // Read from textures
+ // ***********************
+ #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING)
+ vec3 normal = newNormal;
+ #elif !defined(VERTEX_LIGHTING)
+ vec3 normal = vNormal;
+ #if !defined(LOW_QUALITY) && !defined(V_TANGENT)
+ normal = normalize(normal);
+ #endif
+ #endif
+
+ #ifdef SPECULARMAP
+ vec4 specularColor = texture2D(m_SpecularMap, newTexCoord);
+ #else
+ vec4 specularColor = vec4(1.0);
+ #endif
+
+ #ifdef LIGHTMAP
+ vec3 lightMapColor;
+ #ifdef SEPARATE_TEXCOORD
+ lightMapColor = texture2D(m_LightMap, texCoord2).rgb;
+ #else
+ lightMapColor = texture2D(m_LightMap, texCoord).rgb;
+ #endif
+ specularColor.rgb *= lightMapColor;
+ diffuseColor.rgb *= lightMapColor;
+ #endif
+
+ #ifdef VERTEX_LIGHTING
+ vec2 light = vertexLightValues.xy;
+ #ifdef COLORRAMP
+ light.x = texture2D(m_ColorRamp, vec2(light.x, 0.0)).r;
+ light.y = texture2D(m_ColorRamp, vec2(light.y, 0.0)).r;
+ #endif
+
+ #ifndef USE_SCATTERING
+ gl_FragColor.rgb = AmbientSum * diffuseColor.rgb +
+ DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) +
+ SpecularSum * specularColor.rgb * vec3(light.y);
+ #else
+ vec3 color = AmbientSum * diffuseColor.rgb +
+ DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) +
+ SpecularSum * specularColor.rgb * vec3(light.y);
+ gl_FragColor.rgb = calculateGroundColor(vec4(color, 1.0)).rgb;
+ #endif
+ #else
+ vec4 lightDir = vLightDir;
+ lightDir.xyz = normalize(lightDir.xyz);
+ vec3 viewDir = normalize(vViewDir);
+
+ vec2 light = computeLighting(normal, viewDir, lightDir.xyz) * spotFallOff;
+ #ifdef COLORRAMP
+ diffuseColor.rgb *= texture2D(m_ColorRamp, vec2(light.x, 0.0)).rgb;
+ specularColor.rgb *= texture2D(m_ColorRamp, vec2(light.y, 0.0)).rgb;
+ #endif
+
+ // Workaround, since it is not possible to modify varying variables
+ vec4 SpecularSum2 = vec4(SpecularSum, 1.0);
+ #ifdef USE_REFLECTION
+ vec4 refColor = Optics_GetEnvColor(m_EnvMap, refVec.xyz);
+
+ // Interpolate light specularity toward reflection color
+ // Multiply result by specular map
+ specularColor = mix(SpecularSum2 * light.y, refColor, refVec.w) * specularColor;
+
+ SpecularSum2 = vec4(1.0);
+ light.y = 1.0;
+ #endif
+
+ #ifndef USE_SCATTERING
+ gl_FragColor.rgb = AmbientSum * diffuseColor.rgb +
+ DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) +
+ SpecularSum * specularColor.rgb * vec3(light.y);
+ #else
+ vec3 color = AmbientSum * diffuseColor.rgb +
+ DiffuseSum.rgb * diffuseColor.rgb * vec3(light.x) +
+ SpecularSum * specularColor.rgb * vec3(light.y);
+ gl_FragColor.rgb = calculateGroundColor(vec4(color, 1.0)).rgb;
+ #endif
+
+ #endif
+ gl_FragColor.a = alpha;
+}
diff --git a/simarboreal/assets/MatDefs/MultiResolution.j3md b/simarboreal/assets/MatDefs/MultiResolution.j3md
new file mode 100644
index 0000000..fab96ea
--- /dev/null
+++ b/simarboreal/assets/MatDefs/MultiResolution.j3md
@@ -0,0 +1,352 @@
+MaterialDef Phong Lighting {
+
+ MaterialParameters {
+
+ // Compute vertex lighting in the shader
+ // For better performance
+ Boolean VertexLighting
+
+ // Use more efficent algorithms to improve performance
+ Boolean LowQuality
+
+ // Improve quality at the cost of performance
+ Boolean HighQuality
+
+ // Output alpha from the diffuse map
+ Boolean UseAlpha
+
+ // Alpha threshold for fragment discarding
+ Float AlphaDiscardThreshold (AlphaTestFallOff)
+
+ // Normal map is in BC5/ATI2n/LATC/3Dc compression format
+ Boolean LATC
+
+ // Use the provided ambient, diffuse, and specular colors
+ Boolean UseMaterialColors
+
+ // Activate shading along the tangent, instead of the normal
+ // Requires tangent data to be available on the model.
+ Boolean VTangent
+
+ // Use minnaert diffuse instead of lambert
+ Boolean Minnaert
+
+ // Use ward specular instead of phong
+ Boolean WardIso
+
+ // Use vertex color as an additional diffuse color.
+ Boolean UseVertexColor
+
+ // Ambient color
+ Color Ambient (MaterialAmbient)
+
+ // Diffuse color
+ Color Diffuse (MaterialDiffuse)
+
+ // Specular color
+ Color Specular (MaterialSpecular)
+
+ // Specular power/shininess
+ Float Shininess (MaterialShininess) : 1
+
+ // Diffuse map
+ Texture2D DiffuseMap
+
+ // Diffuse map
+ Texture2D BackgroundDiffuseMap
+
+ // Diffuse map
+ Texture2D NoiseMap
+
+ // Normal map
+ Texture2D NormalMap
+
+ // Specular/gloss map
+ Texture2D SpecularMap
+
+ // Parallax/height map
+ Texture2D ParallaxMap
+
+ //Set to true is parallax map is stored in the alpha channel of the normal map
+ Boolean PackedNormalParallax
+
+ //Sets the relief height for parallax mapping
+ Float ParallaxHeight : 0.05
+
+ //Set to true to activate Steep Parallax mapping
+ Boolean SteepParallax
+
+ // Texture that specifies alpha values
+ Texture2D AlphaMap
+
+ // Color ramp, will map diffuse and specular values through it.
+ Texture2D ColorRamp
+
+ // Texture of the glowing parts of the material
+ Texture2D GlowMap
+
+ // Set to Use Lightmap
+ Texture2D LightMap
+
+ // Set to use TexCoord2 for the lightmap sampling
+ Boolean SeparateTexCoord
+
+ // The glow color of the object
+ Color GlowColor
+
+ // Parameters for fresnel
+ // X = bias
+ // Y = scale
+ // Z = power
+ Vector3 FresnelParams
+
+ // Env Map for reflection
+ TextureCubeMap EnvMap
+
+ // the env map is a spheremap and not a cube map
+ Boolean EnvMapAsSphereMap
+
+ //shadows
+ Int FilterMode
+ Boolean HardwareShadows
+
+ Texture2D ShadowMap0
+ Texture2D ShadowMap1
+ Texture2D ShadowMap2
+ Texture2D ShadowMap3
+ //pointLights
+ Texture2D ShadowMap4
+ Texture2D ShadowMap5
+
+ Float ShadowIntensity
+ Vector4 Splits
+ Vector2 FadeInfo
+
+ Matrix4 LightViewProjectionMatrix0
+ Matrix4 LightViewProjectionMatrix1
+ Matrix4 LightViewProjectionMatrix2
+ Matrix4 LightViewProjectionMatrix3
+ //pointLight
+ Matrix4 LightViewProjectionMatrix4
+ Matrix4 LightViewProjectionMatrix5
+ Vector3 LightPos
+
+ Float PCFEdge
+ Float ShadowMapSize
+
+ // For hardware skinning
+ Int NumberOfBones
+ Matrix4Array BoneMatrices
+
+
+ // Ground scattering parameters
+ Boolean UseScattering
+ Vector3 SunPosition
+ Float Exposure
+ Float KmESun
+ Float InnerRadius
+ Float RadiusScale
+ Float PlanetScale : 1
+ Vector3 InvWavelengthsKrESun
+ Float AverageDensityScale
+ Float InvAverageDensityHeight;
+ Vector3 KWavelengths4PI;
+
+ }
+
+ Technique {
+
+ LightMode MultiPass
+
+ VertexShader GLSL110: MatDefs/MultiResolution.vert
+ FragmentShader GLSL110: MatDefs/MultiResolution.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ NormalMatrix
+ WorldViewMatrix
+ ViewMatrix
+ CameraPosition
+ WorldMatrix
+ }
+
+ Defines {
+ LATC : LATC
+ VERTEX_COLOR : UseVertexColor
+ VERTEX_LIGHTING : VertexLighting
+ ATTENUATION : Attenuation
+ MATERIAL_COLORS : UseMaterialColors
+ V_TANGENT : VTangent
+ MINNAERT : Minnaert
+ WARDISO : WardIso
+ LOW_QUALITY : LowQuality
+ HQ_ATTENUATION : HighQuality
+
+ DIFFUSEMAP : DiffuseMap
+ NORMALMAP : NormalMap
+ SPECULARMAP : SpecularMap
+ PARALLAXMAP : ParallaxMap
+ NORMALMAP_PARALLAX : PackedNormalParallax
+ STEEP_PARALLAX : SteepParallax
+ ALPHAMAP : AlphaMap
+ COLORRAMP : ColorRamp
+ LIGHTMAP : LightMap
+ SEPARATE_TEXCOORD : SeparateTexCoord
+
+ USE_REFLECTION : EnvMap
+ SPHERE_MAP : SphereMap
+
+ NUM_BONES : NumberOfBones
+
+ USE_SCATTERING : UseScattering
+ }
+ }
+
+ Technique PreShadow {
+
+ VertexShader GLSL110 : Common/MatDefs/Shadow/PreShadow.vert
+ FragmentShader GLSL110 : Common/MatDefs/Shadow/PreShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldViewMatrix
+ }
+
+ Defines {
+ COLOR_MAP : ColorMap
+ DISCARD_ALPHA : AlphaDiscardThreshold
+ NUM_BONES : NumberOfBones
+ }
+
+ ForcedRenderState {
+ FaceCull Off
+ DepthTest On
+ DepthWrite On
+ PolyOffset 5 3
+ ColorWrite Off
+ }
+
+ }
+
+
+ Technique PostShadow15{
+ VertexShader GLSL150: Common/MatDefs/Shadow/PostShadow15.vert
+ FragmentShader GLSL150: Common/MatDefs/Shadow/PostShadow15.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ }
+
+ Defines {
+ HARDWARE_SHADOWS : HardwareShadows
+ FILTER_MODE : FilterMode
+ PCFEDGE : PCFEdge
+ DISCARD_ALPHA : AlphaDiscardThreshold
+ COLOR_MAP : ColorMap
+ SHADOWMAP_SIZE : ShadowMapSize
+ FADE : FadeInfo
+ PSSM : Splits
+ POINTLIGHT : LightViewProjectionMatrix5
+ NUM_BONES : NumberOfBones
+ }
+
+ ForcedRenderState {
+ Blend Modulate
+ DepthWrite Off
+ PolyOffset -0.1 0
+ }
+ }
+
+ Technique PostShadow{
+ VertexShader GLSL110: Common/MatDefs/Shadow/PostShadow.vert
+ FragmentShader GLSL110: Common/MatDefs/Shadow/PostShadow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldMatrix
+ }
+
+ Defines {
+ HARDWARE_SHADOWS : HardwareShadows
+ FILTER_MODE : FilterMode
+ PCFEDGE : PCFEdge
+ DISCARD_ALPHA : AlphaDiscardThreshold
+ COLOR_MAP : ColorMap
+ SHADOWMAP_SIZE : ShadowMapSize
+ FADE : FadeInfo
+ PSSM : Splits
+ POINTLIGHT : LightViewProjectionMatrix5
+ NUM_BONES : NumberOfBones
+ }
+
+ ForcedRenderState {
+ Blend Modulate
+ DepthWrite Off
+ PolyOffset -0.1 0
+ }
+ }
+
+ Technique PreNormalPass {
+
+ VertexShader GLSL110 : Common/MatDefs/SSAO/normal.vert
+ FragmentShader GLSL110 : Common/MatDefs/SSAO/normal.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ WorldViewMatrix
+ NormalMatrix
+ }
+
+ Defines {
+ DIFFUSEMAP_ALPHA : DiffuseMap
+ NUM_BONES : NumberOfBones
+ }
+
+ }
+
+ Technique GBuf {
+
+ VertexShader GLSL110: Common/MatDefs/Light/GBuf.vert
+ FragmentShader GLSL110: Common/MatDefs/Light/GBuf.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ NormalMatrix
+ WorldViewMatrix
+ WorldMatrix
+ }
+
+ Defines {
+ VERTEX_COLOR : UseVertexColor
+ MATERIAL_COLORS : UseMaterialColors
+ V_TANGENT : VTangent
+ MINNAERT : Minnaert
+ WARDISO : WardIso
+
+ DIFFUSEMAP : DiffuseMap
+ NORMALMAP : NormalMap
+ SPECULARMAP : SpecularMap
+ PARALLAXMAP : ParallaxMap
+ }
+ }
+
+ Technique Glow {
+
+ VertexShader GLSL110: Common/MatDefs/Misc/Unshaded.vert
+ FragmentShader GLSL110: Common/MatDefs/Light/Glow.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+
+ Defines {
+ NEED_TEXCOORD1
+ HAS_GLOWMAP : GlowMap
+ HAS_GLOWCOLOR : GlowColor
+
+ NUM_BONES : NumberOfBones
+ }
+ }
+
+}
diff --git a/simarboreal/assets/MatDefs/MultiResolution.vert b/simarboreal/assets/MatDefs/MultiResolution.vert
new file mode 100644
index 0000000..d5cbeed
--- /dev/null
+++ b/simarboreal/assets/MatDefs/MultiResolution.vert
@@ -0,0 +1,237 @@
+#define ATTENUATION
+//#define HQ_ATTENUATION
+
+#import "Common/ShaderLib/Skinning.glsllib"
+#import "MatDefs/VertScattering.glsllib"
+
+
+uniform mat4 g_WorldViewProjectionMatrix;
+uniform mat4 g_WorldViewMatrix;
+uniform mat4 g_WorldMatrix;
+uniform mat3 g_NormalMatrix;
+uniform mat4 g_ViewMatrix;
+uniform vec3 g_CameraPosition;
+
+uniform vec4 m_Ambient;
+uniform vec4 m_Diffuse;
+uniform vec4 m_Specular;
+uniform float m_Shininess;
+
+uniform vec4 g_LightColor;
+uniform vec4 g_LightPosition;
+uniform vec4 g_AmbientLightColor;
+
+varying vec2 texCoord;
+#ifdef SEPARATE_TEXCOORD
+ varying vec2 texCoord2;
+ attribute vec2 inTexCoord2;
+#endif
+
+varying vec3 AmbientSum;
+varying vec4 DiffuseSum;
+varying vec3 SpecularSum;
+
+varying float z;
+
+attribute vec3 inPosition;
+attribute vec2 inTexCoord;
+attribute vec3 inNormal;
+
+varying vec3 lightVec;
+//varying vec4 spotVec;
+
+#ifdef VERTEX_COLOR
+ attribute vec4 inColor;
+#endif
+
+#ifndef VERTEX_LIGHTING
+ attribute vec4 inTangent;
+
+ #ifndef NORMALMAP
+ varying vec3 vNormal;
+ #endif
+ //varying vec3 vPosition;
+ varying vec3 vViewDir;
+ varying vec4 vLightDir;
+#else
+ varying vec2 vertexLightValues;
+ uniform vec4 g_LightDirection;
+#endif
+
+#ifdef USE_REFLECTION
+ uniform vec3 g_CameraPosition;
+ uniform mat4 g_WorldMatrix;
+
+ uniform vec3 m_FresnelParams;
+ varying vec4 refVec;
+
+
+ /**
+ * Input:
+ * attribute inPosition
+ * attribute inNormal
+ * uniform g_WorldMatrix
+ * uniform g_CameraPosition
+ *
+ * Output:
+ * varying refVec
+ */
+ void computeRef(in vec4 modelSpacePos){
+ vec3 worldPos = (g_WorldMatrix * modelSpacePos).xyz;
+
+ vec3 I = normalize( g_CameraPosition - worldPos ).xyz;
+ vec3 N = normalize( (g_WorldMatrix * vec4(inNormal, 0.0)).xyz );
+
+ refVec.xyz = reflect(I, N);
+ refVec.w = m_FresnelParams.x + m_FresnelParams.y * pow(1.0 + dot(I, N), m_FresnelParams.z);
+ }
+#endif
+
+// JME3 lights in world space
+void lightComputeDir(in vec3 worldPos, in vec4 color, in vec4 position, out vec4 lightDir){
+ float posLight = step(0.5, color.w);
+ vec3 tempVec = position.xyz * sign(posLight - 0.5) - (worldPos * posLight);
+ lightVec = tempVec;
+ #ifdef ATTENUATION
+ float dist = length(tempVec);
+ lightDir.w = clamp(1.0 - position.w * dist * posLight, 0.0, 1.0);
+ lightDir.xyz = tempVec / vec3(dist);
+ #else
+ lightDir = vec4(normalize(tempVec), 1.0);
+ #endif
+}
+
+#ifdef VERTEX_LIGHTING
+ float lightComputeDiffuse(in vec3 norm, in vec3 lightdir){
+ return max(0.0, dot(norm, lightdir));
+ }
+
+ float lightComputeSpecular(in vec3 norm, in vec3 viewdir, in vec3 lightdir, in float shiny){
+ if (shiny <= 1.0){
+ return 0.0;
+ }
+ #ifndef LOW_QUALITY
+ vec3 H = (viewdir + lightdir) * vec3(0.5);
+ return pow(max(dot(H, norm), 0.0), shiny);
+ #else
+ return 0.0;
+ #endif
+ }
+
+vec2 computeLighting(in vec3 wvPos, in vec3 wvNorm, in vec3 wvViewDir, in vec4 wvLightPos){
+ vec4 lightDir;
+ lightComputeDir(wvPos, g_LightColor, wvLightPos, lightDir);
+ float spotFallOff = 1.0;
+ if(g_LightDirection.w != 0.0){
+ vec3 L=normalize(lightVec.xyz);
+ vec3 spotdir = normalize(g_LightDirection.xyz);
+ float curAngleCos = dot(-L, spotdir);
+ float innerAngleCos = floor(g_LightDirection.w) * 0.001;
+ float outerAngleCos = fract(g_LightDirection.w);
+ float innerMinusOuter = innerAngleCos - outerAngleCos;
+ spotFallOff = clamp((curAngleCos - outerAngleCos) / innerMinusOuter, 0.0, 1.0);
+ }
+ float diffuseFactor = lightComputeDiffuse(wvNorm, lightDir.xyz);
+ float specularFactor = lightComputeSpecular(wvNorm, wvViewDir, lightDir.xyz, m_Shininess);
+ //specularFactor *= step(0.01, diffuseFactor);
+ return vec2(diffuseFactor, specularFactor) * vec2(lightDir.w)*spotFallOff;
+ }
+#endif
+
+void main(){
+ vec4 modelSpacePos = vec4(inPosition, 1.0);
+ vec3 modelSpaceNorm = inNormal;
+
+ #ifndef VERTEX_LIGHTING
+ vec3 modelSpaceTan = inTangent.xyz;
+ #endif
+
+ #ifdef NUM_BONES
+ #ifndef VERTEX_LIGHTING
+ Skinning_Compute(modelSpacePos, modelSpaceNorm, modelSpaceTan);
+ #else
+ Skinning_Compute(modelSpacePos, modelSpaceNorm);
+ #endif
+ #endif
+
+ #ifdef USE_SCATTERING
+ vec4 wPos = g_WorldMatrix * modelSpacePos;
+ calculateVertexGroundScattering(wPos.xyz, g_CameraPosition);
+ #endif
+
+ gl_Position = g_WorldViewProjectionMatrix * modelSpacePos;
+ texCoord = inTexCoord;
+ #ifdef SEPARATE_TEXCOORD
+ texCoord2 = inTexCoord2;
+ #endif
+
+ vec3 wvPosition = (g_WorldViewMatrix * modelSpacePos).xyz;
+
+ z = length(wvPosition);
+
+ vec3 wvNormal = normalize(g_NormalMatrix * modelSpaceNorm);
+ vec3 viewDir = normalize(-wvPosition);
+
+ //vec4 lightColor = g_LightColor[gl_InstanceID];
+ //vec4 lightPos = g_LightPosition[gl_InstanceID];
+ //vec4 wvLightPos = (g_ViewMatrix * vec4(lightPos.xyz, lightColor.w));
+ //wvLightPos.w = lightPos.w;
+
+ vec4 wvLightPos = (g_ViewMatrix * vec4(g_LightPosition.xyz,clamp(g_LightColor.w,0.0,1.0)));
+ wvLightPos.w = g_LightPosition.w;
+ vec4 lightColor = g_LightColor;
+
+ #if defined(NORMALMAP) && !defined(VERTEX_LIGHTING)
+ vec3 wvTangent = normalize(g_NormalMatrix * modelSpaceTan);
+ vec3 wvBinormal = cross(wvNormal, wvTangent);
+
+ mat3 tbnMat = mat3(wvTangent, wvBinormal * -inTangent.w,wvNormal);
+
+ //vPosition = wvPosition * tbnMat;
+ //vViewDir = viewDir * tbnMat;
+ vViewDir = -wvPosition * tbnMat;
+ lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir);
+ vLightDir.xyz = (vLightDir.xyz * tbnMat).xyz;
+ #elif !defined(VERTEX_LIGHTING)
+ vNormal = wvNormal;
+
+ //vPosition = wvPosition;
+ vViewDir = viewDir;
+
+ lightComputeDir(wvPosition, lightColor, wvLightPos, vLightDir);
+
+ #ifdef V_TANGENT
+ vNormal = normalize(g_NormalMatrix * inTangent.xyz);
+ vNormal = -cross(cross(vLightDir.xyz, vNormal), vNormal);
+ #endif
+ #endif
+
+ //computing spot direction in view space and unpacking spotlight cos
+// spotVec = (g_ViewMatrix * vec4(g_LightDirection.xyz, 0.0) );
+// spotVec.w = floor(g_LightDirection.w) * 0.001;
+// lightVec.w = fract(g_LightDirection.w);
+
+ lightColor.w = 1.0;
+ #ifdef MATERIAL_COLORS
+ AmbientSum = (m_Ambient * g_AmbientLightColor).rgb;
+ DiffuseSum = m_Diffuse * lightColor;
+ SpecularSum = (m_Specular * lightColor).rgb;
+ #else
+ AmbientSum = vec3(0.2, 0.2, 0.2) * g_AmbientLightColor.rgb; // Default: ambient color is dark gray
+ DiffuseSum = lightColor;
+ SpecularSum = vec3(0.0);
+ #endif
+
+ #ifdef VERTEX_COLOR
+ AmbientSum *= inColor.rgb;
+ DiffuseSum *= inColor;
+ #endif
+
+ #ifdef VERTEX_LIGHTING
+ vertexLightValues = computeLighting(wvPosition, wvNormal, viewDir, wvLightPos);
+ #endif
+
+ #ifdef USE_REFLECTION
+ computeRef(modelSpacePos);
+ #endif
+}
diff --git a/simarboreal/assets/MatDefs/Null.frag b/simarboreal/assets/MatDefs/Null.frag
new file mode 100644
index 0000000..04a9e2e
--- /dev/null
+++ b/simarboreal/assets/MatDefs/Null.frag
@@ -0,0 +1,10 @@
+#import "Common/ShaderLib/MultiSample.glsllib"
+
+uniform COLORTEXTURE m_Texture;
+varying vec2 texCoord;
+
+void main() {
+ vec4 texVal = getColor(m_Texture, texCoord);
+ gl_FragColor = texVal;
+}
+
diff --git a/simarboreal/assets/MatDefs/Null.j3md b/simarboreal/assets/MatDefs/Null.j3md
new file mode 100644
index 0000000..98fe07f
--- /dev/null
+++ b/simarboreal/assets/MatDefs/Null.j3md
@@ -0,0 +1,24 @@
+MaterialDef Depth Blur {
+
+ MaterialParameters {
+ Int NumSamples
+ Int NumSamplesDepth
+ Texture2D Texture
+ Texture2D DepthTexture
+ }
+
+ Technique {
+ VertexShader GLSL100: Common/MatDefs/Post/Post.vert
+ FragmentShader GLSL100: MatDefs/Null.frag
+
+ WorldParameters {
+ WorldViewProjectionMatrix
+ }
+
+ Defines {
+ RESOLVE_MS : NumSamples
+ RESOLVE_DEPTH_MS : NumSamplesDepth
+ }
+ }
+
+}
diff --git a/simarboreal/assets/MatDefs/Shadows.frag b/simarboreal/assets/MatDefs/Shadows.frag
new file mode 100644
index 0000000..5041007
--- /dev/null
+++ b/simarboreal/assets/MatDefs/Shadows.frag
@@ -0,0 +1,72 @@
+#import "Common/ShaderLib/MultiSample.glsllib"
+
+//#define SHOW_BOX
+//#define SHOW_DELTA
+
+uniform vec2 g_FrustumNearFar;
+uniform vec4 g_ViewPort;
+
+uniform vec4 m_ShadowColor;
+uniform COLORTEXTURE m_FrameTexture;
+uniform DEPTHTEXTURE m_DepthTexture;
+
+varying vec3 texCoord;
+varying vec3 vViewDir;
+varying vec3 boxScale;
+
+void main(){
+ vec4 color = vec4(1.0);
+
+ vec2 uv = vec2(gl_FragCoord.x/g_ViewPort.z, gl_FragCoord.y/g_ViewPort.w);
+
+ float zBuffer = getDepth( m_DepthTexture, uv ).r;
+
+ //
+ // z_buffer_value = a + b / z;
+ //
+ // Where:
+ // a = zFar / ( zFar - zNear )
+ // b = zFar * zNear / ( zNear - zFar )
+ // z = distance from the eye to the object
+ //
+ // Which means:
+ // zb - a = b / z;
+ // z * (zb - a) = b
+ // z = b / (zb - a)
+ //
+ float a = g_FrustumNearFar.y / (g_FrustumNearFar.y - g_FrustumNearFar.x);
+ float b = g_FrustumNearFar.y * g_FrustumNearFar.x / (g_FrustumNearFar.x - g_FrustumNearFar.y);
+ float z = b / (zBuffer - a);
+
+ float us = b / (gl_FragCoord.z - a);
+
+ float modelScale = 1.0;
+
+ float delta = (z-us) * modelScale;
+
+ #if defined(SHOW_DELTA)
+ color = vec4(delta, 0.0, 0.0, 1.0);
+ #elif defined(SHOW_BOX)
+ color = vec4(texCoord * boxScale,1.0);
+ #else
+
+ vec3 view = normalize(vViewDir);
+ vec3 scene = texCoord + view * delta;
+ vec3 stu = scene * boxScale;
+
+ float xTex = (0.5 - stu.x) * 2.0;
+ float zTex = (0.5 - stu.z) * 2.0;
+ float t = stu.y;
+
+ float low = (t - 0.75) * 1.33333;
+ float hi = (t - 0.75) * 4.0;
+ float yTex = low * step(t, 0.75) + hi * step(0.75, t);
+
+ float col = sqrt((xTex * xTex) + (zTex * zTex) + (yTex * yTex));
+ float shadow = (1.0 - col);
+ color = vec4(m_ShadowColor);
+ color.a *= clamp(shadow, 0.0, 0.8);
+ #endif
+
+ gl_FragColor = color;
+}
diff --git a/simarboreal/assets/MatDefs/Shadows.j3md b/simarboreal/assets/MatDefs/Shadows.j3md
new file mode 100644
index 0000000..62880c5
--- /dev/null
+++ b/simarboreal/assets/MatDefs/Shadows.j3md
@@ -0,0 +1,29 @@
+MaterialDef Simple Shadows {
+
+ MaterialParameters {
+ Int NumSamples
+ Int NumSamplesDepth
+
+ Color ShadowColor
+
+ Texture2D FrameTexture
+ Texture2D DepthTexture
+ }
+
+ Technique {
+ VertexShader GLSL120: MatDefs/Shadows.vert
+ FragmentShader GLSL130: MatDefs/Shadows.frag
+
+ WorldParameters {
+ ViewProjectionMatrix
+ FrustumNearFar
+ ViewPort
+ }
+
+ Defines {
+ RESOLVE_MS : NumSamples
+ RESOLVE_DEPTH_MS : NumSamplesDepth
+ }
+ }
+
+}
diff --git a/simarboreal/assets/MatDefs/Shadows.vert b/simarboreal/assets/MatDefs/Shadows.vert
new file mode 100644
index 0000000..e04b6ba
--- /dev/null
+++ b/simarboreal/assets/MatDefs/Shadows.vert
@@ -0,0 +1,22 @@
+
+uniform mat4 g_ViewProjectionMatrix;
+
+attribute vec3 inPosition; // the world position
+attribute vec3 inTexCoord; // the model space position, relative to a corner
+attribute vec3 inTexCoord2; // the x,y,z scale to get from model space to 0->1 space
+attribute vec3 inNormal; // the view direction in model-space
+
+varying vec3 texCoord;
+varying vec3 vViewDir;
+varying vec3 boxScale;
+
+
+
+void main(){
+ vec4 modelSpacePos = vec4(inPosition, 1.0);
+ gl_Position = g_ViewProjectionMatrix * modelSpacePos;
+
+ vViewDir = inNormal;
+ texCoord = inTexCoord;
+ boxScale = inTexCoord2;
+}
diff --git a/simarboreal/assets/Models/female-parts.j3o b/simarboreal/assets/Models/female-parts.j3o
new file mode 100644
index 0000000..16f027a
Binary files /dev/null and b/simarboreal/assets/Models/female-parts.j3o differ
diff --git a/simarboreal/assets/Models/male-parts-no-bones.j3o b/simarboreal/assets/Models/male-parts-no-bones.j3o
new file mode 100644
index 0000000..1767f16
Binary files /dev/null and b/simarboreal/assets/Models/male-parts-no-bones.j3o differ
diff --git a/simarboreal/assets/Textures/brown-dirt-norm.jpg b/simarboreal/assets/Textures/brown-dirt-norm.jpg
new file mode 100644
index 0000000..3ae610d
Binary files /dev/null and b/simarboreal/assets/Textures/brown-dirt-norm.jpg differ
diff --git a/simarboreal/assets/Textures/grass-flat.jpg b/simarboreal/assets/Textures/grass-flat.jpg
new file mode 100644
index 0000000..103be5d
Binary files /dev/null and b/simarboreal/assets/Textures/grass-flat.jpg differ
diff --git a/simarboreal/assets/Textures/grass.jpg b/simarboreal/assets/Textures/grass.jpg
new file mode 100644
index 0000000..c07909b
Binary files /dev/null and b/simarboreal/assets/Textures/grass.jpg differ
diff --git a/simarboreal/assets/Textures/test-pattern.png b/simarboreal/assets/Textures/test-pattern.png
new file mode 100644
index 0000000..a245956
Binary files /dev/null and b/simarboreal/assets/Textures/test-pattern.png differ
diff --git a/simarboreal/build.gradle b/simarboreal/build.gradle
new file mode 100644
index 0000000..1a7358e
--- /dev/null
+++ b/simarboreal/build.gradle
@@ -0,0 +1,31 @@
+plugins {
+ id 'application'
+}
+
+application {
+ mainClass = 'com.simsilica.arboreal.TreeEditor'
+ applicationDefaultJvmArgs = ['-Xmx512m', '-XX:MaxDirectMemorySize=512m']
+}
+
+sourceSets.main.resources {
+ srcDirs += 'src/main/java'
+ exclude '**/*.java'
+ exclude '**/*.tmp'
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ runtimeOnly files('assets')
+}
+
+tasks.register('extractNatives', Copy) {
+ from zipTree(file('libs/jME3-lwjgl-natives.jar'))
+ into "${buildDir}/natives"
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+}
+
+run {
+ dependsOn extractNatives
+ workingDir = rootDir
+ jvmArgs "-Djava.library.path=${buildDir}/natives"
+}
diff --git a/simarboreal/libs/Lemur.jar b/simarboreal/libs/Lemur.jar
new file mode 100644
index 0000000..2001759
Binary files /dev/null and b/simarboreal/libs/Lemur.jar differ
diff --git a/simarboreal/libs/LemurProps.jar b/simarboreal/libs/LemurProps.jar
new file mode 100644
index 0000000..d577b90
Binary files /dev/null and b/simarboreal/libs/LemurProps.jar differ
diff --git a/simarboreal/libs/Pager.jar b/simarboreal/libs/Pager.jar
new file mode 100644
index 0000000..36971f7
Binary files /dev/null and b/simarboreal/libs/Pager.jar differ
diff --git a/simarboreal/libs/SimArboreal.jar b/simarboreal/libs/SimArboreal.jar
new file mode 100644
index 0000000..e10714c
Binary files /dev/null and b/simarboreal/libs/SimArboreal.jar differ
diff --git a/simarboreal/libs/arboreal-assets.jar b/simarboreal/libs/arboreal-assets.jar
new file mode 100644
index 0000000..b47bc2f
Binary files /dev/null and b/simarboreal/libs/arboreal-assets.jar differ
diff --git a/simarboreal/libs/assets.jar b/simarboreal/libs/assets.jar
new file mode 100644
index 0000000..d16ffb8
Binary files /dev/null and b/simarboreal/libs/assets.jar differ
diff --git a/simarboreal/libs/groovy-all-2.1.9.jar b/simarboreal/libs/groovy-all-2.1.9.jar
new file mode 100644
index 0000000..8f1eb05
Binary files /dev/null and b/simarboreal/libs/groovy-all-2.1.9.jar differ
diff --git a/simarboreal/libs/guava-12.0.jar b/simarboreal/libs/guava-12.0.jar
new file mode 100644
index 0000000..fefd6b2
Binary files /dev/null and b/simarboreal/libs/guava-12.0.jar differ
diff --git a/simarboreal/libs/jME3-core.jar b/simarboreal/libs/jME3-core.jar
new file mode 100644
index 0000000..585ea7d
Binary files /dev/null and b/simarboreal/libs/jME3-core.jar differ
diff --git a/simarboreal/libs/jME3-desktop.jar b/simarboreal/libs/jME3-desktop.jar
new file mode 100644
index 0000000..528edfc
Binary files /dev/null and b/simarboreal/libs/jME3-desktop.jar differ
diff --git a/simarboreal/libs/jME3-effects.jar b/simarboreal/libs/jME3-effects.jar
new file mode 100644
index 0000000..4c09a75
Binary files /dev/null and b/simarboreal/libs/jME3-effects.jar differ
diff --git a/simarboreal/libs/jME3-lwjgl-natives.jar b/simarboreal/libs/jME3-lwjgl-natives.jar
new file mode 100644
index 0000000..8d55d8f
Binary files /dev/null and b/simarboreal/libs/jME3-lwjgl-natives.jar differ
diff --git a/simarboreal/libs/jME3-lwjgl.jar b/simarboreal/libs/jME3-lwjgl.jar
new file mode 100644
index 0000000..d7093c0
Binary files /dev/null and b/simarboreal/libs/jME3-lwjgl.jar differ
diff --git a/simarboreal/libs/jME3-plugins.jar b/simarboreal/libs/jME3-plugins.jar
new file mode 100644
index 0000000..b341a01
Binary files /dev/null and b/simarboreal/libs/jME3-plugins.jar differ
diff --git a/simarboreal/libs/jinput.jar b/simarboreal/libs/jinput.jar
new file mode 100644
index 0000000..4c75006
Binary files /dev/null and b/simarboreal/libs/jinput.jar differ
diff --git a/simarboreal/libs/log4j-1.2.12.jar b/simarboreal/libs/log4j-1.2.12.jar
new file mode 100644
index 0000000..9b5a720
Binary files /dev/null and b/simarboreal/libs/log4j-1.2.12.jar differ
diff --git a/simarboreal/libs/lwjgl.jar b/simarboreal/libs/lwjgl.jar
new file mode 100644
index 0000000..f76c937
Binary files /dev/null and b/simarboreal/libs/lwjgl.jar differ
diff --git a/simarboreal/libs/meta-jb-json-1.0.1.jar b/simarboreal/libs/meta-jb-json-1.0.1.jar
new file mode 100644
index 0000000..3a70510
Binary files /dev/null and b/simarboreal/libs/meta-jb-json-1.0.1.jar differ
diff --git a/simarboreal/libs/slf4j-api-1.7.5.jar b/simarboreal/libs/slf4j-api-1.7.5.jar
new file mode 100644
index 0000000..8766455
Binary files /dev/null and b/simarboreal/libs/slf4j-api-1.7.5.jar differ
diff --git a/simarboreal/libs/slf4j-log4j12-1.7.5.jar b/simarboreal/libs/slf4j-log4j12-1.7.5.jar
new file mode 100644
index 0000000..afce5c2
Binary files /dev/null and b/simarboreal/libs/slf4j-log4j12-1.7.5.jar differ
diff --git a/simarboreal/nbproject/assets-impl.xml b/simarboreal/nbproject/assets-impl.xml
new file mode 100644
index 0000000..0a47d8d
--- /dev/null
+++ b/simarboreal/nbproject/assets-impl.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/simarboreal/nbproject/build-impl.xml b/simarboreal/nbproject/build-impl.xml
new file mode 100644
index 0000000..45400b2
--- /dev/null
+++ b/simarboreal/nbproject/build-impl.xml
@@ -0,0 +1,1521 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set platform.home
+ Must set platform.bootcp
+ Must set platform.java
+ Must set platform.javac
+
+ The J2SE Platform is not correctly set up.
+ Your active platform is: ${platform.active}, but the corresponding property "platforms.${platform.active}.home" is not found in the project's properties files.
+ Either open the project in the IDE and setup the Platform with the same name or add it manually.
+ For example like this:
+ ant -Duser.properties.file=<path_to_property_file> jar (where you put the property "platforms.${platform.active}.home" in a .properties file)
+ or ant -Dplatforms.${platform.active}.home=<path_to_JDK_home> jar (where no properties file is used)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set src.resources.dir
+ Must set src.java.dir
+ Must set build.dir
+ Must set dist.dir
+ Must set build.classes.dir
+ Must set dist.javadoc.dir
+ Must set build.test.classes.dir
+ Must set build.test.results.dir
+ Must set build.classes.excludes
+ Must set dist.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No tests executed.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must set JVM to use for profiling in profiler.info.jvm
+ Must set profiler agent JVM arguments in profiler.info.jvmargs.agent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ To run this application from the command line without Ant, try:
+
+ ${platform.java} -jar "${dist.jar.resolved}"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set debug.class
+
+
+
+
+ Must select one file in the IDE or set debug.class
+
+
+
+
+ Must set fix.includes
+
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set profile.class
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+
+
+
+
+ This target only works when run from inside the NetBeans IDE.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+
+ Must select some files in the IDE or set test.includes
+
+
+
+
+ Must select one file in the IDE or set run.class
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set javac.includes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Some tests failed; see details above.
+
+
+
+
+
+
+
+
+ Must select some files in the IDE or set test.includes
+
+
+
+ Some tests failed; see details above.
+
+
+
+ Must select some files in the IDE or set test.class
+ Must select some method in the IDE or set test.method
+
+
+
+ Some tests failed; see details above.
+
+
+
+
+ Must select one file in the IDE or set test.class
+
+
+
+ Must select one file in the IDE or set test.class
+ Must select some method in the IDE or set test.method
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+ Must select one file in the IDE or set applet.url
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/simarboreal/nbproject/genfiles.properties b/simarboreal/nbproject/genfiles.properties
new file mode 100644
index 0000000..b988779
--- /dev/null
+++ b/simarboreal/nbproject/genfiles.properties
@@ -0,0 +1,8 @@
+build.xml.data.CRC32=94bf7c61
+build.xml.script.CRC32=79a29eb7
+build.xml.stylesheet.CRC32=958a1d3e@1.32.1.45
+# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
+# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
+nbproject/build-impl.xml.data.CRC32=0f2662fc
+nbproject/build-impl.xml.script.CRC32=bb94fa75
+nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48
diff --git a/simarboreal/nbproject/project.properties b/simarboreal/nbproject/project.properties
new file mode 100644
index 0000000..c6301cb
--- /dev/null
+++ b/simarboreal/nbproject/project.properties
@@ -0,0 +1,124 @@
+annotation.processing.enabled=true
+annotation.processing.enabled.in.editor=false
+annotation.processing.processors.list=
+annotation.processing.run.all.processors=true
+ant.customtasks.libs=launch4j
+application.desc=Editor for the tree parameters necessary for generating trees.
+application.homepage=https://code.google.com/p/simsilica-tools/
+application.splash=C:\\Development\\google\\simsilica-tools\\trunk\\SimArboreal-Editor\\TreeEditor-Splash.png
+application.title=SimArboreal-Editor
+application.vendor=Simsilica, LLC
+assets.jar.name=assets.jar
+assets.excludes=**/*.j3odata,**/*.mesh,**/*.skeleton,**/*.mesh.xml,**/*.skeleton.xml,**/*.scene,**/*.material,**/*.obj,**/*.mtl,**/*.3ds,**/*.dae,**/*.blend,**/*.blend*[0-9],**/.backups/**,**/*.psd
+assets.folder.name=assets
+assets.compress=true
+build.classes.dir=${build.dir}/classes
+build.classes.excludes=**/*.java,**/*.form,**/.backups/**
+# This directory is removed when the project is cleaned:
+build.dir=build
+build.generated.dir=${build.dir}/generated
+build.generated.sources.dir=${build.dir}/generated-sources
+# Only compile against the classpath explicitly listed here:
+build.sysclasspath=ignore
+build.test.classes.dir=${build.dir}/test/classes
+build.test.results.dir=${build.dir}/test/results
+compile.on.save=true
+# Uncomment to specify the preferred debugger connection transport:
+#debug.transport=dt_socket
+debug.classpath=\
+ ${run.classpath}
+debug.test.classpath=\
+ ${run.test.classpath}
+# This directory is removed when the project is cleaned:
+dist.dir=dist
+dist.jar=${dist.dir}/${application.title}.jar
+dist.javadoc.dir=${dist.dir}/javadoc
+endorsed.classpath=
+excludes=
+file.reference.arboreal-assets.jar=..\\SimArboreal\\dist\\lib\\arboreal-assets.jar
+file.reference.groovy-all-2.1.9.jar=lib\\groovy-all-2.1.9.jar
+file.reference.guava-12.0.jar=lib\\guava-12.0.jar
+file.reference.log4j-1.2.12.jar=lib\\log4j-1.2.12.jar
+file.reference.meta-jb-json-1.0.1.jar=lib\\meta-jb-json-1.0.1.jar
+file.reference.simfx-assets.jar=..\\SimFX\\dist\\lib\\simfx-assets.jar
+file.reference.slf4j-api-1.7.5.jar=lib\\slf4j-api-1.7.5.jar
+file.reference.slf4j-log4j12-1.7.5.jar=lib\\slf4j-log4j12-1.7.5.jar
+includes=**
+jar.compress=false
+javac.classpath=\
+ ${reference.SimArboreal.jar}:\
+ ${libs.jme3-lwjgl.classpath}:\
+ ${libs.jme3-effects.classpath}:\
+ ${libs.jme3-desktop.classpath}:\
+ ${file.reference.arboreal-assets.jar}:\
+ ${reference.Pager.jar}:\
+ ${reference.SimFX.jar}:\
+ ${libs.jme3-core.classpath}:\
+ ${reference.Lemur.jar}:\
+ ${reference.LemurProps.jar}:\
+ ${file.reference.groovy-all-2.1.9.jar}:\
+ ${file.reference.guava-12.0.jar}:\
+ ${file.reference.log4j-1.2.12.jar}:\
+ ${file.reference.meta-jb-json-1.0.1.jar}:\
+ ${file.reference.slf4j-api-1.7.5.jar}:\
+ ${file.reference.slf4j-log4j12-1.7.5.jar}:\
+ ${file.reference.simfx-assets.jar}
+# Space-separated list of extra javac options
+javac.compilerargs=
+javac.deprecation=false
+javac.processorpath=\
+ ${javac.classpath}
+javac.source=1.6
+javac.target=1.6
+javac.test.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}
+javadoc.additionalparam=
+javadoc.author=false
+javadoc.encoding=${source.encoding}
+javadoc.noindex=false
+javadoc.nonavbar=false
+javadoc.notree=false
+javadoc.private=false
+javadoc.splitindex=true
+javadoc.use=true
+javadoc.version=false
+javadoc.windowtitle=
+jaxbwiz.endorsed.dirs="${netbeans.home}/../ide12/modules/ext/jaxb/api"
+jnlp.codebase.type=local
+jnlp.descriptor=application
+jnlp.enabled=false
+jnlp.offline-allowed=false
+jnlp.signed=false
+launch4j.exe.enabled=true
+linux.launcher.enabled=true
+mac.app.enabled=true
+main.class=com.simsilica.arboreal.TreeEditor
+meta.inf.dir=${src.dir}/META-INF
+manifest.file=MANIFEST.MF
+mkdist.disabled=false
+platform.active=JDK_1.7
+project.Lemur=../../Lemur
+project.LemurProps=../../Lemur/extensions/LemurProps
+project.Pager=../Pager
+project.SimArboreal=../SimArboreal
+project.SimFX=../SimFX
+reference.Lemur.jar=${project.Lemur}/dist/Lemur.jar
+reference.LemurProps.jar=${project.LemurProps}/dist/LemurProps.jar
+reference.Pager.jar=${project.Pager}/dist/Pager.jar
+reference.SimArboreal.jar=${project.SimArboreal}/dist/SimArboreal.jar
+reference.SimFX.jar=${project.SimFX}/dist/SimFX.jar
+run.classpath=\
+ ${javac.classpath}:\
+ ${build.classes.dir}:\
+ ${assets.folder.name}
+# Space-separated list of JVM arguments used when running the project
+# (you may also define separate properties like run-sys-prop.name=value instead of -Dname=value
+# or test-sys-prop.name=value to set system properties for unit tests):
+run.jvmargs=-Xmx512m -XX:MaxDirectMemorySize=512m
+run.test.classpath=\
+ ${javac.test.classpath}:\
+ ${build.test.classes.dir}
+source.encoding=UTF-8
+src.java.dir=src\\main\\java
+src.resources.dir=src\\main\\resources
diff --git a/simarboreal/nbproject/project.xml b/simarboreal/nbproject/project.xml
new file mode 100644
index 0000000..1e7497b
--- /dev/null
+++ b/simarboreal/nbproject/project.xml
@@ -0,0 +1,71 @@
+
+
+ org.netbeans.modules.java.j2seproject
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SimArboreal-Editor
+
+
+
+
+
+
+
+
+
+ Lemur
+ jar
+
+ jar
+ clean
+ jar
+
+
+ LemurProps
+ jar
+
+ jar
+ clean
+ jar
+
+
+ Pager
+ jar
+
+ jar
+ clean
+ jar
+
+
+ SimArboreal
+ jar
+
+ jar
+ clean
+ jar
+
+
+ SimFX
+ jar
+
+ jar
+ clean
+ jar
+
+
+
+
diff --git a/simarboreal/release/SimArboreal-Editor-Linux.zip b/simarboreal/release/SimArboreal-Editor-Linux.zip
new file mode 100644
index 0000000..5976a29
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux.zip differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.jar b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.jar
new file mode 100644
index 0000000..e3e1d65
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.sh b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.sh
new file mode 100644
index 0000000..61026ac
--- /dev/null
+++ b/simarboreal/release/SimArboreal-Editor-Linux/SimArboreal-Editor.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+java -Xmx512m -XX:MaxDirectMemorySize=512m -jar SimArboreal-Editor.jar
+
\ No newline at end of file
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/Lemur.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/Lemur.jar
new file mode 100644
index 0000000..2001759
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/Lemur.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/LemurProps.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/LemurProps.jar
new file mode 100644
index 0000000..d577b90
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/LemurProps.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/Pager.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/Pager.jar
new file mode 100644
index 0000000..36971f7
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/Pager.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/SimArboreal.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/SimArboreal.jar
new file mode 100644
index 0000000..e10714c
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/SimArboreal.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/arboreal-assets.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/arboreal-assets.jar
new file mode 100644
index 0000000..b47bc2f
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/arboreal-assets.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/assets.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/assets.jar
new file mode 100644
index 0000000..d16ffb8
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/assets.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/groovy-all-2.1.9.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/groovy-all-2.1.9.jar
new file mode 100644
index 0000000..8f1eb05
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/groovy-all-2.1.9.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/guava-12.0.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/guava-12.0.jar
new file mode 100644
index 0000000..fefd6b2
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/guava-12.0.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-core.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-core.jar
new file mode 100644
index 0000000..585ea7d
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-core.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-desktop.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-desktop.jar
new file mode 100644
index 0000000..528edfc
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-desktop.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-effects.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-effects.jar
new file mode 100644
index 0000000..4c09a75
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-effects.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl-natives.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl-natives.jar
new file mode 100644
index 0000000..8d55d8f
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl-natives.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl.jar
new file mode 100644
index 0000000..d7093c0
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-lwjgl.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-plugins.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-plugins.jar
new file mode 100644
index 0000000..b341a01
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jME3-plugins.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/jinput.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/jinput.jar
new file mode 100644
index 0000000..4c75006
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/jinput.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/log4j-1.2.12.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/log4j-1.2.12.jar
new file mode 100644
index 0000000..9b5a720
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/log4j-1.2.12.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/lwjgl.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/lwjgl.jar
new file mode 100644
index 0000000..f76c937
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/lwjgl.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/meta-jb-json-1.0.1.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/meta-jb-json-1.0.1.jar
new file mode 100644
index 0000000..3a70510
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/meta-jb-json-1.0.1.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-api-1.7.5.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-api-1.7.5.jar
new file mode 100644
index 0000000..8766455
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-api-1.7.5.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-log4j12-1.7.5.jar b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-log4j12-1.7.5.jar
new file mode 100644
index 0000000..afce5c2
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/lib/slf4j-log4j12-1.7.5.jar differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/liblwjgl64.so b/simarboreal/release/SimArboreal-Editor-Linux/liblwjgl64.so
new file mode 100644
index 0000000..314b892
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/liblwjgl64.so differ
diff --git a/simarboreal/release/SimArboreal-Editor-Linux/libopenal64.so b/simarboreal/release/SimArboreal-Editor-Linux/libopenal64.so
new file mode 100644
index 0000000..e0693c0
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Linux/libopenal64.so differ
diff --git a/simarboreal/release/SimArboreal-Editor-MacOSX.zip b/simarboreal/release/SimArboreal-Editor-MacOSX.zip
new file mode 100644
index 0000000..c4b1545
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-MacOSX.zip differ
diff --git a/simarboreal/release/SimArboreal-Editor-Windows.zip b/simarboreal/release/SimArboreal-Editor-Windows.zip
new file mode 100644
index 0000000..553af11
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor-Windows.zip differ
diff --git a/simarboreal/release/SimArboreal-Editor-changelog.txt b/simarboreal/release/SimArboreal-Editor-changelog.txt
new file mode 100644
index 0000000..5777520
--- /dev/null
+++ b/simarboreal/release/SimArboreal-Editor-changelog.txt
@@ -0,0 +1,71 @@
+Revision ???
+-------------
+- Added a dependency to the SimFX package and converted to use
+ its LightingState and SkyState (with scattering)
+- Grass plane supports atmospherics and a toggle was added to
+ the visualization options panel.
+- Added atmospheric support for trees along with a toggle.
+
+
+
+Revision 143
+-------------
+- Added an action to save the tree atlas images
+ as PNG files.
+- Fixed how the atlas textures are generated so that they
+ save as embedded textures in the j3o.
+- Fixed impostor meshes to use short buffers instead of int.
+- Added toggleable noise-based wind
+- Added a video recodring option F12
+- Added editors for the tree wind-related parameters
+- Changed the tree parameters file extension to just plain
+ .simap The old simap.json extension is still supported for
+ loading and the file format itself hasn't changed.
+
+
+Revision 126
+-------------
+- Reworked the FileActionsState to better allow embedding
+ in external applications. Save and load methods were added
+ and the buttons are now not added to the UI unless the state
+ is enabled. This should help facilitate a parallel JME SDK
+ plug-in effort.
+
+
+Revision 94
+------------
+- Moved RollupPanel and TabbedPanel out into Lemur core.
+- Moved the builder classes out into the new simsilica-tools Pager
+ library.
+- Moved PropertyPanel into its own Lemur extension project LemurProps.
+- Converted to use the now-standard Lemur 'glass' style with just a few
+ local custom extensions.
+- Moved the Builderstate out to the builder project.
+
+
+Revision 69
+-------------
+- Added Y offset parameter that is separate from trunk
+ height.
+- Added LOD support including two mesh reduction strategies:
+ Flat-Poly : renders the tree branches as a set of axis-aligned
+ billboarded flat quads.
+ Impostor : renders a single quad with a view-direction indexed texture.
+ (Note: impostors currently don't save properly to the j3o)
+- Better visual separate of child properies in the UI.
+- Reorganized UI to include outer rollup panels to separate vis
+ settings from tree parameters.
+- Added an avatar toggle to the UI.
+- Added shadow intensity and lighting direction settings to the
+ UI.
+- Added a simplified 'drop shadow' filter that can be enabled instead
+ of regular shadows.
+- File write operations now warn before overwriting existing files.
+- Cleaned out the wire frame meshes from the exported j3o during save.
+- Renamed the tree geometry elements to make more sense when viewing
+ the tree object in something like Scene Composer.
+
+
+Revision 33
+-------------
+- Initial release
\ No newline at end of file
diff --git a/simarboreal/release/SimArboreal-Editor.jar b/simarboreal/release/SimArboreal-Editor.jar
new file mode 100644
index 0000000..e3e1d65
Binary files /dev/null and b/simarboreal/release/SimArboreal-Editor.jar differ
diff --git a/simarboreal/settings.gradle.bak b/simarboreal/settings.gradle.bak
new file mode 100644
index 0000000..917403c
--- /dev/null
+++ b/simarboreal/settings.gradle.bak
@@ -0,0 +1 @@
+rootProject.name = 'sim-arboreal-editor'
diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/AtlasGeneratorState.java b/simarboreal/src/main/java/com/simsilica/arboreal/AtlasGeneratorState.java
new file mode 100644
index 0000000..ff99bf2
--- /dev/null
+++ b/simarboreal/src/main/java/com/simsilica/arboreal/AtlasGeneratorState.java
@@ -0,0 +1,576 @@
+/*
+ * $Id$
+ *
+ * Copyright (c) 2014, Simsilica, LLC
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.simsilica.arboreal;
+
+import com.simsilica.builder.BuilderState;
+import com.jme3.app.Application;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.BitmapText;
+import com.jme3.light.AmbientLight;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.queue.RenderQueue.Bucket;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Node;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.debug.WireBox;
+import com.jme3.scene.shape.Quad;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Image;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture2D;
+import com.jme3.util.BufferUtils;
+import com.simsilica.arboreal.mesh.BillboardedLeavesMeshGenerator;
+import com.simsilica.arboreal.mesh.SkinnedTreeMeshGenerator;
+import com.simsilica.arboreal.mesh.Vertex;
+import com.simsilica.builder.Builder;
+import com.simsilica.builder.BuilderReference;
+import com.simsilica.lemur.GuiGlobals;
+import com.simsilica.lemur.core.VersionedReference;
+import com.simsilica.lemur.event.BaseAppState;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ *
+ * @author Paul Speed
+ */
+public class AtlasGeneratorState extends BaseAppState {
+
+ static Logger log = LoggerFactory.getLogger(AtlasGeneratorState.class);
+
+ private VersionedReference treeParametersRef;
+ private Material treeMaterial;
+ private Material leafMaterial;
+
+ private Builder builder;
+ private AtlasTreeBuilderReference builderRef;
+
+ private Mesh trunkMesh;
+ private Mesh leafMesh;
+
+ private FrameBuffer diffuseFb;
+ private FrameBuffer normalFb;
+ private CellView[] cellViews = new CellView[8];
+ private Image diffuseMap;
+ private Texture2D diffuseTexture;
+ private Image normalMap;
+ private Texture2D normalTexture;
+ private int needTextureUpdate;
+
+ private BitmapFont font;
+
+ private boolean debugTextures = false;
+
+ private boolean useNormalMaps = true;
+
+ public AtlasGeneratorState() {
+ }
+
+ public Image getDiffuseMap() {
+ return diffuseMap;
+ }
+
+ public Image getNormalMap() {
+ return normalMap;
+ }
+
+ protected Image createFrameBufferImage( FrameBuffer fb ) {
+ int width = fb.getWidth();
+ int height = fb.getHeight();
+ int size = width * height * 4;
+ ByteBuffer buffer = BufferUtils.createByteBuffer(size);
+ Image.Format format = fb.getColorBuffer().getFormat();
+
+ // I guess readFrameBuffer always writes in the same
+ // format regardless of the frame buffer format
+ format = Format.BGRA8;
+ return new Image(format, width, height, buffer);
+ }
+
+ protected void updateTextures() {
+
+ Renderer renderer = getApplication().getRenderer();
+ if( diffuseMap == null ) {
+ diffuseMap = createFrameBufferImage(diffuseFb);
+ diffuseTexture = new Texture2D(diffuseMap);
+ getState(ForestGridState.class).getImpostorMaterial().setTexture("DiffuseMap", diffuseTexture);
+ }
+ renderer.readFrameBuffer(diffuseFb, diffuseMap.getData(0));
+ diffuseMap.setUpdateNeeded();
+
+ if( normalMap == null ) {
+ normalMap = createFrameBufferImage(normalFb);
+ normalTexture = new Texture2D(normalMap);
+ if( useNormalMaps ) {
+ getState(ForestGridState.class).getImpostorMaterial().setTexture("NormalMap", normalTexture);
+ }
+ }
+ renderer.readFrameBuffer(normalFb, normalMap.getData(0));
+ normalMap.setUpdateNeeded();
+
+ needTextureUpdate = 0;
+ }
+
+ @Override
+ protected void initialize( Application app ) {
+
+ this.treeParametersRef = getState(TreeParametersState.class).getTreeParametersRef();
+ this.treeMaterial = getState(ForestGridState.class).getTreeMaterial();
+ this.leafMaterial = getState(ForestGridState.class).getLeafMaterial();
+
+ this.builder = getState(BuilderState.class).getBuilder();
+ this.builderRef = new AtlasTreeBuilderReference();
+
+
+ this.font = GuiGlobals.getInstance().loadFont("Interface/Fonts/Default.fnt");
+
+ Camera camera = app.getCamera().clone();
+ camera.resize(256, 256, true);
+ camera.resize(1024, 256, false);
+
+
+ FrameBuffer fb1 = new FrameBuffer(1024, 256, 1);
+ diffuseFb = fb1;
+ Texture2D fbTex1 = new Texture2D(1024, 256, Format.RGBA8);
+ fb1.setDepthBuffer(Format.Depth);
+ fb1.setColorTexture(fbTex1);
+
+ FrameBuffer fb2 = new FrameBuffer(1024, 256, 1);
+ normalFb = fb2;
+ Texture2D fbTex2 = new Texture2D(1024, 256, Format.RGBA8);
+ fb2.setDepthBuffer(Format.Depth);
+ fb2.setColorTexture(fbTex2);
+
+
+ if( debugTextures ) {
+ Quad testQuad = new Quad(512, 128);
+ Geometry testGeom = new Geometry("test", testQuad);
+ Material mat = GuiGlobals.getInstance().createMaterial(fbTex1, false).getMaterial();
+ testGeom.setMaterial(mat);
+ ((TreeEditor)app).getGuiNode().attachChild(testGeom);
+
+ testQuad = new Quad(512, 128);
+ testGeom = new Geometry("test", testQuad);
+ testGeom.setLocalTranslation(0, 128, 0);
+ mat = GuiGlobals.getInstance().createMaterial(fbTex2, false).getMaterial();
+ testGeom.setMaterial(mat);
+ ((TreeEditor)app).getGuiNode().attachChild(testGeom);
+
+ updateTextures();
+
+ testQuad = new Quad(512, 128);
+ testGeom = new Geometry("test", testQuad);
+ testGeom.setLocalTranslation(0, 256, 0);
+ mat = GuiGlobals.getInstance().createMaterial(diffuseTexture, false).getMaterial();
+ testGeom.setMaterial(mat);
+ ((TreeEditor)app).getGuiNode().attachChild(testGeom);
+
+ testQuad = new Quad(512, 128);
+ testGeom = new Geometry("test", testQuad);
+ testGeom.setLocalTranslation(0, 384, 0);
+ mat = GuiGlobals.getInstance().createMaterial(normalTexture, false).getMaterial();
+ testGeom.setMaterial(mat);
+ ((TreeEditor)app).getGuiNode().attachChild(testGeom);
+ }
+
+ DirectionalLight sun = new DirectionalLight();
+ //sun.setDirection(new Vector3f(0, -1f, -1).normalizeLocal());
+ sun.setDirection(new Vector3f(0, 0, -1).normalizeLocal());
+
+ AmbientLight ambient = new AmbientLight();
+
+ if( useNormalMaps ) {
+ sun.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1));
+ ambient.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1));
+ } else {
+ sun.setColor(new ColorRGBA(1, 1, 1, 1));
+ ambient.setColor(new ColorRGBA(0.25f, 0.25f, 0.25f, 1));
+ }
+
+ //-x * FastMath.TWO_PI - FastMath.QUARTER_PI
+ // The texture quads actually run a, c, d, b starting with
+ // the +, + quadrant
+ cellViews[0] = new CellView(fb1, camera, sun, ambient, FastMath.QUARTER_PI, 0.25f * 0);
+ cellViews[1] = new CellView(fb1, camera, sun, ambient, -FastMath.QUARTER_PI, 0.25f * 1);
+ cellViews[2] = new CellView(fb1, camera, sun, ambient, FastMath.PI - FastMath.QUARTER_PI, 0.25f * 2);
+ cellViews[3] = new CellView(fb1, camera, sun, ambient, FastMath.PI + FastMath.QUARTER_PI, 0.25f * 3);
+
+ cellViews[4] = new NormalMapCellView(fb2, camera, sun, ambient, FastMath.QUARTER_PI, 0.25f * 0);
+ cellViews[5] = new NormalMapCellView(fb2, camera, sun, ambient, -FastMath.QUARTER_PI, 0.25f * 1);
+ cellViews[6] = new NormalMapCellView(fb2, camera, sun, ambient, FastMath.PI - FastMath.QUARTER_PI, 0.25f * 2);
+ cellViews[7] = new NormalMapCellView(fb2, camera, sun, ambient, FastMath.PI + FastMath.QUARTER_PI, 0.25f * 3);
+
+ }
+
+ @Override
+ protected void cleanup( Application app ) {
+ for( CellView view : cellViews ) {
+ app.getRenderManager().removeMainView(view.getViewPort());
+ }
+ }
+
+ @Override
+ protected void enable() {
+ }
+
+ @Override
+ protected void disable() {
+ }
+
+ protected void updateTree( Mesh trunkMesh, Mesh leafMesh ) {
+ if( this.trunkMesh == trunkMesh ) {
+ return;
+ }
+
+ releaseMesh(this.trunkMesh);
+ releaseMesh(this.leafMesh);
+ this.trunkMesh = trunkMesh;
+ this.leafMesh = leafMesh;
+
+ for( CellView view : cellViews ) {
+ if( view != null ) {
+ view.updateMesh(trunkMesh, leafMesh);
+ }
+ }
+
+ // Texture updates need to happen one frame late...
+ // but we get the notification that we need the check
+ // early in _this_ frame. ie: updateTree() is called
+ // before our update(), render(). If we want to render
+ // a frame later then we need to skip this frame before
+ // updating textures.
+ needTextureUpdate = 2;
+ }
+
+ protected void releaseMesh( Mesh mesh ) {
+ if( mesh == null ) {
+ return;
+ }
+
+ // Delete the old buffers
+ for( VertexBuffer vb : mesh.getBufferList() ) {
+ if( log.isTraceEnabled() ) {
+ log.trace("--destroying buffer:" + vb);
+ }
+ BufferUtils.destroyDirectBuffer( vb.getData() );
+ }
+ }
+
+ private float nextUpdateCheck = 0.1f;
+ private float lastTpf;
+ @Override
+ public void update( float tpf ) {
+ lastTpf = tpf;
+
+ nextUpdateCheck += tpf;
+ if( nextUpdateCheck <= 0.1f ) {
+ return;
+ }
+ nextUpdateCheck = 0;
+
+ boolean changed = treeParametersRef.update();
+ if( changed ) {
+ builder.build(builderRef);
+ }
+ }
+
+ @Override
+ public void render( RenderManager rm ) {
+ if( cellViews != null ) {
+ // We update the logical state here because it is
+ // done after the other updates. So if another app
+ // state or control has modified our root then we
+ // are guaranteed to run after.
+ for( CellView view : cellViews ) {
+ if( view != null ) {
+ view.update(lastTpf);
+ }
+ }
+ }
+
+ // Texture updates need to happen one frame later...
+ // but we get the notification that we need the check
+ // early in _this_ frame.
+ if( needTextureUpdate > 0 ) {
+ needTextureUpdate--;
+ if( needTextureUpdate == 0 ) {
+ updateTextures();
+ }
+ }
+ }
+
+ private class CellView {
+ private ViewPort viewport;
+ private Camera camera;
+ private Node root;
+ private Mesh leafMesh;
+ private Mesh trunkMesh;
+ private Geometry trunkGeom;
+ private Geometry leafGeom;
+ private Geometry wireBounds;
+ private boolean debugBounds = false;
+ private boolean debugCell = false;
+
+ public CellView( FrameBuffer fb, Camera templateCamera, DirectionalLight sun, AmbientLight ambient, float angle, float x ) {
+
+ this.camera = templateCamera.clone();
+ camera.setViewPort(x, x + 0.25f, 0, 1);
+
+ this.root = new Node("CellRoot:" + x );
+ this.viewport = getApplication().getRenderManager().createMainView("AtlasCell[" + x + "]", camera);
+ this.viewport.setOutputFrameBuffer(fb);
+ this.root.rotate(0, -angle, 0);
+
+ if( debugCell ) {
+ BitmapText label = new BitmapText(font);
+ label.setText("u:" + x + "\na:" + angle);
+ label.setLocalScale(0.01f);
+ Quaternion labelRot = root.getLocalRotation().inverse();
+ label.setLocalRotation(labelRot);
+ label.setLocalTranslation(labelRot.mult(new Vector3f(0, 1, 2)));
+ root.attachChild(label);
+ }
+
+ viewport.attachScene(root);
+ root.addLight(sun);
+ root.addLight(ambient);
+
+ viewport.setClearFlags(true, true, true);
+ viewport.setBackgroundColor(new ColorRGBA(0, 0, 0, 0));
+ this.camera.lookAtDirection(new Vector3f(0, 0, -1), Vector3f.UNIT_Y);
+ }
+
+ public ViewPort getViewPort() {
+ return viewport;
+ }
+
+ public void update( float tpf ) {
+ root.updateLogicalState(tpf);
+ root.updateGeometricState();
+ }
+
+ protected Material getTreeMaterial() {
+ return treeMaterial;
+ }
+
+ protected Material getLeafMaterial() {
+ return leafMaterial;
+ }
+
+ public void updateMesh( Mesh trunkMesh, Mesh leafMesh ) {
+ if( trunkGeom == null ) {
+ // Create it
+ trunkGeom = new Geometry("Trunk", trunkMesh);
+ trunkGeom.setMaterial(getTreeMaterial());
+ root.attachChild(trunkGeom);
+ } else {
+ // Just swap out the mesh
+ trunkGeom.setMesh(trunkMesh);
+ }
+ this.trunkMesh = trunkMesh;
+ this.leafMesh = leafMesh;
+ if( leafMesh == null ) {
+ if( leafGeom != null ) {
+ leafGeom.removeFromParent();
+ leafGeom = null;
+ }
+ } else {
+ if( leafGeom == null ) {
+ // Create it
+ leafGeom = new Geometry("Leaves", leafMesh);
+ leafGeom.setMaterial(getLeafMaterial());
+ leafGeom.setQueueBucket(Bucket.Transparent);
+ root.attachChild(leafGeom);
+ } else {
+ // Just swap out the mesh
+ leafGeom.setMesh(leafMesh);
+ }
+ }
+ updateCamera();
+ }
+
+ protected void updateCamera() {
+
+ BoundingBox bb = (BoundingBox)trunkMesh.getBound();
+ if( leafGeom != null ) {
+ BoundingBox bb2 = (BoundingBox)leafMesh.getBound();
+ bb = (BoundingBox)bb.merge(bb2);
+ }
+
+ Vector3f min = bb.getMin(null);
+ Vector3f max = bb.getMax(null);
+
+ float xSize = Math.max(Math.abs(min.x), Math.abs(max.x));
+ float ySize = max.y - min.y;
+ float zSize = Math.max(Math.abs(min.z), Math.abs(max.z));
+
+ float size = ySize * 0.5f;
+ size = Math.max(size, xSize);
+ size = Math.max(size, zSize);
+
+ // In the projection matrix, [1][1] should be:
+ // (2 * Zn) / camHeight
+ // where Zn is distance to near plane.
+ float m11 = camera.getViewProjectionMatrix().m11;
+
+ // We want our position to be such that
+ // 'size' is otherwise = cameraHeight when rendered.
+ float z = m11 * size;
+
+ // Add the z extents so that we adjust for the near plane
+ // of the bounding box... well we will be rotating so
+ // let's just be sure and take the max of x and z
+ //float offset = Math.max(bb.getXExtent(), bb.getZExtent());
+ //z += offset;
+ // This creates problems because it makes way too much
+ // space around the tree. A proper solution would require
+ // a bunch of math and in the end would also have to be duplicated
+ // on the quad generation side or somehow stored with the atlas.
+
+ Vector3f center = bb.getCenter();
+
+ float sizeOffset = size - (ySize*0.5f);
+
+ Vector3f camLoc = new Vector3f(0, center.y + sizeOffset, z);
+ camera.setLocation(camLoc);
+
+ if( debugBounds ) {
+ WireBox box;
+ if( wireBounds == null ) {
+ box = new WireBox();
+ wireBounds = new Geometry("wire box", box);
+ Material mat = GuiGlobals.getInstance().createMaterial(ColorRGBA.Yellow, false).getMaterial();
+ wireBounds.setMaterial(mat);
+ root.attachChild(wireBounds);
+ } else {
+ box = (WireBox)wireBounds.getMesh();
+ }
+ box.updatePositions(bb.getXExtent(), bb.getYExtent(), bb.getZExtent());
+ box.setBound(new BoundingBox(new Vector3f(0,0,0), 0, 0, 0));
+ wireBounds.setLocalTranslation(bb.getCenter());
+ wireBounds.setLocalRotation(leafGeom.getLocalRotation());
+ }
+ }
+
+ }
+
+
+ private class NormalMapCellView extends CellView {
+ public NormalMapCellView( FrameBuffer fb, Camera templateCamera, DirectionalLight sun, AmbientLight ambient, float angle, float x ) {
+ super(fb, templateCamera, sun, ambient, angle, x);
+ }
+
+ @Override
+ protected Material getTreeMaterial() {
+ Material normalMaterial = treeMaterial.clone();
+ normalMaterial.selectTechnique("PreNormalPass", getApplication().getRenderManager());
+ return normalMaterial;
+ }
+
+ @Override
+ protected Material getLeafMaterial() {
+ Material normalMaterial = leafMaterial.clone();
+ normalMaterial.selectTechnique("PreNormalPass", getApplication().getRenderManager());
+ return normalMaterial;
+ }
+ }
+
+
+ private class AtlasTreeBuilderReference implements BuilderReference {
+
+ private Mesh trunkMesh;
+ private Mesh leafMesh;
+
+ @Override
+ public int getPriority() {
+ // A relatively low priority
+ return 100;
+ }
+
+ @Override
+ public void build() {
+
+ TreeParameters treeParameters = treeParametersRef.get();
+
+ TreeGenerator treeGen = new TreeGenerator();
+ Tree tree = treeGen.generateTree(treeParameters);
+
+ SkinnedTreeMeshGenerator meshGen = new SkinnedTreeMeshGenerator();
+
+ List tips = new ArrayList();
+ trunkMesh = meshGen.generateMesh(tree,
+ treeParameters.getLod(0),
+ treeParameters.getYOffset(),
+ treeParameters.getTextureURepeat(),
+ treeParameters.getTextureVScale(),
+ tips);
+
+ if( treeParameters.getGenerateLeaves() ) {
+ BillboardedLeavesMeshGenerator leafGen = new BillboardedLeavesMeshGenerator();
+ leafMesh = leafGen.generateMesh(tips, treeParameters.getLeafScale());
+ } else {
+ leafMesh = null;
+ }
+ }
+
+ @Override
+ public void apply() {
+ // Set the new trunk
+ updateTree(trunkMesh, leafMesh);
+ }
+
+ @Override
+ public void release() {
+
+ }
+ }
+}
diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/AvatarState.java b/simarboreal/src/main/java/com/simsilica/arboreal/AvatarState.java
new file mode 100644
index 0000000..9fa6459
--- /dev/null
+++ b/simarboreal/src/main/java/com/simsilica/arboreal/AvatarState.java
@@ -0,0 +1,133 @@
+/*
+ * ${Id}
+ *
+ * Copyright (c) 2014, Simsilica, LLC
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.simsilica.arboreal;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.AssetManager;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.Spatial.CullHint;
+import com.simsilica.lemur.GuiGlobals;
+import com.simsilica.lemur.event.BaseAppState;
+
+
+/**
+ * Shows some sample people for scale.
+ *
+ * @author Paul Speed
+ */
+public class AvatarState extends BaseAppState {
+
+ private Node avatars;
+ private Spatial male;
+ private Spatial female;
+
+ public AvatarState() {
+ }
+
+ public void setShowAvatars( boolean b ) {
+ if( b ) {
+ avatars.setCullHint(CullHint.Inherit);
+ } else {
+ avatars.setCullHint(CullHint.Always);
+ }
+ }
+
+ @Override
+ protected void initialize( Application app ) {
+
+ AssetManager assets = app.getAssetManager();
+
+ // Add an avatar for scale
+ avatars = new Node("Avatars");
+ avatars.move(2, 0, 0);
+
+ female = (Node)assets.loadModel("Models/female-parts.j3o");
+ BoundingBox bb = (BoundingBox)female.getWorldBound();
+ float height = bb.getYExtent() * 2;
+ float femaleScale = 1.62f / height;
+ female.move(0, bb.getYExtent(), 0);
+ female.setLocalScale(femaleScale);
+ Material mat = GuiGlobals.getInstance().createMaterial(ColorRGBA.Gray, true).getMaterial();
+ mat.setColor("Ambient", ColorRGBA.Gray);
+ female.setMaterial(mat);
+ female.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ avatars.attachChild(female);
+
+ // Add an avatar for scale
+ male = (Node)assets.loadModel("Models/male-parts-no-bones.j3o");
+ bb = (BoundingBox)male.getWorldBound();
+ height = bb.getYExtent() * 2;
+ float maleScale = 1.77f / height;
+ male.move(bb.getCenter().negate());
+ male.move(1, bb.getYExtent(), 0);
+ male.setLocalScale(maleScale);
+ male.setMaterial(mat);
+ male.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
+ avatars.attachChild(male);
+
+ // For testing
+ //Spatial tree = assets.loadModel("Models/test1.j3o");
+ //tree.setLocalTranslation(-20, 0, -20);
+ //avatars.attachChild(tree);
+
+
+ TreeOptionsState options = getState(TreeOptionsState.class);
+ options.addOptionToggle("Avatars", this, "setShowAvatars").setChecked(true);
+
+ }
+
+ @Override
+ protected void cleanup( Application app ) {
+ }
+
+ @Override
+ protected void enable() {
+ Node rootNode = ((SimpleApplication)getApplication()).getRootNode();
+ rootNode.attachChild(avatars);
+ }
+
+ @Override
+ protected void disable() {
+ avatars.removeFromParent();
+ }
+}
diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/DebugHudState.java b/simarboreal/src/main/java/com/simsilica/arboreal/DebugHudState.java
new file mode 100644
index 0000000..0f3c766
--- /dev/null
+++ b/simarboreal/src/main/java/com/simsilica/arboreal/DebugHudState.java
@@ -0,0 +1,202 @@
+/*
+ * $Id$
+ *
+ * Copyright (c) 2014, Simsilica, LLC
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.simsilica.arboreal;
+
+import com.jme3.app.Application;
+import com.jme3.app.SimpleApplication;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.util.MemoryUtils;
+import com.simsilica.lemur.Container;
+import com.simsilica.lemur.GuiGlobals;
+import com.simsilica.lemur.HAlignment;
+import com.simsilica.lemur.Label;
+import com.simsilica.lemur.core.VersionedReference;
+import com.simsilica.lemur.event.BaseAppState;
+import com.simsilica.lemur.input.InputMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ *
+ * @author Paul Speed
+ */
+public class DebugHudState extends BaseAppState {
+
+ static Logger log = LoggerFactory.getLogger(DebugHudState.class);
+
+ private VersionedReference worldLoc;
+ private Runtime runtime = Runtime.getRuntime();
+
+ private Label location;
+ private Label memory;
+ private Label directMem;
+
+ private long lastUsedMem;
+ private long lastMeg100;
+ private long lastDirectMem;
+ private long lastDirectMeg100;
+ private long nextUpdate = System.currentTimeMillis() + 16; // 60 FPS max
+ private long nextMemTime = System.currentTimeMillis() + 1000;
+
+ private long frameCounter;
+ private long lastFrameCheck;
+ private double lastFps;
+
+ private Container debugHud;
+
+ public DebugHudState() {
+ }
+
+ public void toggleHud() {
+ setEnabled( !isEnabled() );
+ }
+
+ @Override
+ protected void initialize( Application app ) {
+
+ // Always register for our hot key as long as
+ // we are attached.
+ InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
+ inputMapper.addDelegate( MainFunctions.F_HUD, this, "toggleHud" );
+
+ worldLoc = getState( MovementState.class ).getWorldPosition().createReference();
+
+ debugHud = new Container();
+
+ location = debugHud.addChild(new Label( "000.00 000.00 00.00" ));
+ location.setTextHAlignment( HAlignment.Right );
+ resetLocation();
+
+ memory = debugHud.addChild(new Label( "Mem: 0.0 meg (0.0 %)" ));
+ memory.setTextHAlignment( HAlignment.Right );
+
+ directMem = debugHud.addChild(new Label( "DMem: 0.0 meg / 0" ));
+ directMem.setTextHAlignment( HAlignment.Right );
+ }
+
+ @Override
+ protected void cleanup( Application app ) {
+ InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
+ inputMapper.removeDelegate( MainFunctions.F_HUD, this, "toggleHud" );
+ }
+
+ protected void resetLocation() {
+ Vector3f v = worldLoc.get();
+ String loc = String.format( "%.2f, %.2f, %.2f", v.x, v.y, v.z );
+ location.setText(loc);
+ }
+
+ @Override
+ public void update( float tpf ) {
+
+ frameCounter++;
+ long time = System.currentTimeMillis();
+ if( time < nextUpdate )
+ return;
+ nextUpdate = time + 16; // 60 FPS max
+
+ if( worldLoc.update() ) {
+ resetLocation();
+ }
+
+ /*if( time > lastFrameCheck + 1000 )
+ {
+ long delta = time - lastFrameCheck;
+ lastFrameCheck = time;
+
+ double fps = frameCounter / (delta / 1000.0);
+ frameCounter = 0;
+ if( fps != lastFps )
+ {
+ lastFps = fps;
+ String s = String.format( "FPS: %.2f", fps );
+ fpsText.setText(s);
+ }
+ }*/
+
+
+ // Refresh memory and other things less often----------------------------
+ //-----------------------------------------------------------------------
+ if( time < nextMemTime )
+ return;
+ nextMemTime = time + 1000;
+
+ long usedMemory = runtime.totalMemory() - runtime.freeMemory();
+ if( lastUsedMem != usedMemory ) {
+ lastUsedMem = usedMemory;
+
+ long maxMemory = runtime.maxMemory();
+ long meg100 = (usedMemory * 100) / (1024 * 1024);
+ if( lastMeg100 != meg100 ) {
+ lastMeg100 = meg100;
+ double meg = meg100 / 100.0;
+ double percent = (usedMemory * 100.0 / maxMemory);
+ String mem = String.format( "Mem: %.2f meg (%.1f %%)", meg, percent );
+ memory.setText( mem );
+ }
+ }
+
+ long directUsage = MemoryUtils.getDirectMemoryUsage();
+ if( directUsage != lastDirectMem ) {
+ lastDirectMem = directUsage;
+
+ long meg100 = (directUsage * 100) / (1024 * 1024);
+ if( lastDirectMeg100 != meg100 ) {
+ long directCount = MemoryUtils.getDirectMemoryCount();
+ double meg = meg100 / 100.0;
+ String mem = String.format( "DMem: %.2f meg / %d", meg, directCount );
+ directMem.setText( mem );
+ }
+ }
+
+ Camera cam = getApplication().getCamera();
+ Vector3f pref = debugHud.getPreferredSize();
+ debugHud.setLocalTranslation(cam.getWidth() - pref.x - 10, cam.getHeight() - 10, 0);
+ }
+
+ @Override
+ protected void enable() {
+ ((SimpleApplication)getApplication()).getGuiNode().attachChild(debugHud);
+ }
+
+ @Override
+ protected void disable() {
+ debugHud.removeFromParent();
+ }
+}
diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/DropShadowFilter.java b/simarboreal/src/main/java/com/simsilica/arboreal/DropShadowFilter.java
new file mode 100644
index 0000000..ad19bc9
--- /dev/null
+++ b/simarboreal/src/main/java/com/simsilica/arboreal/DropShadowFilter.java
@@ -0,0 +1,407 @@
+/*
+ * $Id$
+ *
+ * Copyright (c) 2013 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.simsilica.arboreal;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.bounding.BoundingBox;
+import com.jme3.bounding.BoundingSphere;
+import com.jme3.material.Material;
+import com.jme3.material.RenderState.BlendMode;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.post.Filter;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.Camera.FrustumIntersect;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.renderer.queue.GeometryComparator;
+import com.jme3.renderer.queue.GeometryList;
+import com.jme3.renderer.queue.RenderQueue;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.VertexBuffer;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.shadow.ShadowUtil;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Texture;
+import com.jme3.util.BufferUtils;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+
+/**
+ *
+ * @author Paul Speed
+ */
+public class DropShadowFilter extends Filter {
+
+ private static final int VERTS_PER_SHADOW = 8; // one per box corner
+ private static final int TRIS_PER_SHADOW = 12; // two per face
+ private static final int INDEXES_PER_SHADOW = TRIS_PER_SHADOW * 3;
+
+ private static final Vector3f[] BASE_CORNERS = new Vector3f[] {
+ new Vector3f(-1, -1, 1), // 0
+ new Vector3f( 1, -1, 1), // 1
+ new Vector3f( 1, -1, -1), // 2
+ new Vector3f(-1, -1, -1), // 3
+ new Vector3f(-1, 1, 1), // 4
+ new Vector3f( 1, 1, 1), // 5
+ new Vector3f( 1, 1, -1), // 6
+ new Vector3f(-1, 1, -1) // 7
+ };
+
+ private static final short[] BASE_INDEXES = new short[] {
+ // top
+ 4, 5, 6, 4, 6, 7,
+ // bottom
+ 3, 2, 1, 3, 1, 0,
+ // +z
+ 0, 1, 5, 0, 5, 4,
+ // -z
+ 2, 3, 7, 2, 7, 6,
+ // -x
+ 3, 0, 4, 3, 4, 7,
+ // +x
+ 1, 2, 6, 1, 6, 5
+ };
+
+ private Geometry shadowGeom;
+ private Material shadowMaterial;
+ private Mesh mesh;
+ private int maxShadows;
+
+ private ColorRGBA shadowColor = new ColorRGBA(0, 0, 0, 0.75f);
+
+ private VertexBuffer vbPos;
+ private VertexBuffer vbNormal;
+ private VertexBuffer vbTexCoord;
+ private VertexBuffer vbTexCoord2;
+ private VertexBuffer vbIndex;
+
+ private GeometryList casters;
+
+ public DropShadowFilter() {
+ this(500);
+ }
+
+ public DropShadowFilter( int maxShadows ) {
+ this.maxShadows = maxShadows;
+ }
+
+ public void setShadowIntensity( float f ) {
+ shadowColor.a = f;
+ }
+
+ public float getShadowIntensity() {
+ return shadowColor.a;
+ }
+
+ @Override
+ protected boolean isRequiresDepthTexture() {
+ return true;
+ }
+
+ @Override
+ protected void initFilter(AssetManager assets, RenderManager rm, ViewPort vp, int w, int h) {
+
+ // Cheating... side effect of being lazy and using a filter
+ // without actually needing to filter anything.
+ material = new Material( assets, "MatDefs/Null.j3md" );
+
+ mesh = new Mesh();
+
+ // Setup the mesh for the max shadows size
+ mesh.setBuffer( Type.Position, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) );
+ mesh.setBuffer( Type.Normal, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) );
+ mesh.setBuffer( Type.TexCoord, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) );
+ mesh.setBuffer( Type.TexCoord2, 3, BufferUtils.createVector3Buffer(maxShadows * VERTS_PER_SHADOW) );
+ mesh.setBuffer( Type.Index, 3, BufferUtils.createShortBuffer(maxShadows * INDEXES_PER_SHADOW) );
+
+ vbPos = mesh.getBuffer(Type.Position);
+ vbNormal = mesh.getBuffer(Type.Normal);
+ vbTexCoord = mesh.getBuffer(Type.TexCoord);
+ vbTexCoord2 = mesh.getBuffer(Type.TexCoord2);
+ vbIndex = mesh.getBuffer(Type.Index);
+
+
+ shadowGeom = new Geometry("shadowVolumes", mesh);
+ Material m = shadowMaterial = new Material( assets, "MatDefs/Shadows.j3md" );
+ m.setColor( "ShadowColor", shadowColor );
+ m.getAdditionalRenderState().setDepthWrite(false);
+ m.getAdditionalRenderState().setDepthTest(false);
+ m.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
+ shadowGeom.setMaterial(m);
+ shadowGeom.setLocalTranslation(0, 100, 0);
+
+ shadowGeom.updateLogicalState(0.1f);
+ shadowGeom.updateGeometricState();
+
+ // Set our custom comparator for shadow casters
+ casters = new GeometryList(new CasterComparator());
+ }
+
+ @Override
+ protected Material getMaterial() {
+ return material;
+ }
+
+ @Override
+ protected void postFrame( RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer ) {
+
+ RenderQueue rq = viewPort.getQueue();
+ for (Spatial scene : viewPort.getScenes()) {
+ //ShadowUtil.getGeometriesInCamFrustum(scene, viewPort.getCamera(), ShadowMode.Cast, casters);
+ }
+ if( casters.size() == 0 )
+ return;
+
+ Camera cam = viewPort.getCamera();
+ BoundingSphere cullCheck = new BoundingSphere();
+ Vector3f pos = new Vector3f();
+
+ Texture frameTex = prevFilterBuffer.getColorBuffer().getTexture();
+ Texture depthTex = prevFilterBuffer.getDepthBuffer().getTexture();
+ shadowMaterial.setTexture("FrameTexture", frameTex);
+ if( frameTex.getImage().getMultiSamples() > 1 ) {
+ shadowMaterial.setInt("NumSamples", frameTex.getImage().getMultiSamples());
+ } else {
+ shadowMaterial.clearParam("NumSamples");
+ }
+
+ shadowMaterial.setTexture("DepthTexture", depthTex);
+ if( depthTex.getImage().getMultiSamples() > 1 ) {
+ shadowMaterial.setInt("NumSamplesDepth", depthTex.getImage().getMultiSamples());
+ } else {
+ shadowMaterial.clearParam("NumSamplesDepth");
+ }
+
+ int size = casters.size();
+ if( size > maxShadows ) {
+ // Give the shadows their best chance by sorting them.
+ casters.setCamera(cam);
+ casters.sort();
+ }
+
+ FloatBuffer bPos = (FloatBuffer)vbPos.getData().rewind();
+ FloatBuffer bNormal = (FloatBuffer)vbNormal.getData().rewind();
+ FloatBuffer bTexCoord = (FloatBuffer)vbTexCoord.getData().rewind();
+ FloatBuffer bTexCoord2 = (FloatBuffer)vbTexCoord2.getData().rewind();
+ ShortBuffer bIndex = (ShortBuffer)vbIndex.getData().rewind();
+
+
+ Matrix4f viewMatrix = cam.getViewMatrix();
+ Matrix4f worldMatrix = new Matrix4f();
+ Matrix4f worldViewMatrix = new Matrix4f();
+ float[] angles = new float[3];
+ Vector3f vTemp = new Vector3f();
+ Vector3f vert = new Vector3f();
+ Vector3f viewDir = new Vector3f();
+ Vector3f boxScale = new Vector3f();
+
+ int rendered = 0;
+ for( int i = 0; i < size; i++ ) {
+ Geometry g = casters.get(i);
+
+ // Use the geometry bounds. We assumg it is still y-up
+ // and merely rotated. It's a decent enough approximiation
+ // in many cases and will produce better shadows for oblong
+ // objects than a simple round radius would.
+ BoundingBox bounds = (BoundingBox)g.getModelBound();
+
+ float scale = g.getWorldScale().x;
+ float xEx = bounds.getXExtent() * scale;
+ float yEx = bounds.getYExtent() * scale;
+ float zEx = bounds.getZExtent() * scale;
+ float volumeHeight = Math.max(yEx, Math.min(xEx,zEx));
+
+ float xOffset = bounds.getCenter().x * scale;
+ float yOffset = bounds.getCenter().y * scale;
+ float zOffset = bounds.getCenter().z * scale;
+
+ yOffset -= yEx;
+ yOffset -= volumeHeight * 0.5f;
+ yOffset += 0.01f;
+
+ pos.set(g.getWorldTranslation());
+ pos.addLocal(xOffset, yOffset, zOffset);
+
+ // A conservative approximation that works because our shadow volume
+ // is really just a round blob
+ float radius = Math.max(xEx, Math.max(yEx, zEx));
+ cullCheck.setCenter(pos);
+ cullCheck.setRadius(radius);
+
+ int save = cam.getPlaneState();
+ cam.setPlaneState(0);
+ FrustumIntersect intersect = cam.contains(cullCheck);
+ cam.setPlaneState(save);
+
+ if( intersect == FrustumIntersect.Outside ) {
+ continue;
+ }
+
+ boxScale.set(0.5f/xEx, 0.5f/volumeHeight, 0.5f/zEx);
+
+ Quaternion quat = g.getWorldRotation();
+ angles = quat.toAngles(angles);
+
+ Quaternion rotation = new Quaternion().fromAngles(0, angles[1], 0);
+ Quaternion invRotation = rotation.inverse();
+ worldMatrix.setTranslation(pos);
+ worldMatrix.setRotationQuaternion(rotation);
+
+ worldViewMatrix.set(viewMatrix);
+ worldViewMatrix.multLocal(worldMatrix);
+
+ // Setup the vertexes for each corner
+ for( int j = 0; j < VERTS_PER_SHADOW; j++ ) {
+ vTemp.set(BASE_CORNERS[j].x * xEx,
+ BASE_CORNERS[j].y * volumeHeight,
+ BASE_CORNERS[j].z * zEx);
+
+ // Get the transformed coordinate in world space
+ vert = worldMatrix.mult(vTemp, vert);
+ bPos.put(vert.x).put(vert.y).put(vert.z);
+
+ // Now calculate the view direction
+ vert = vert.subtractLocal(cam.getLocation());
+ vert.normalizeLocal();
+ viewDir = invRotation.mult(vert, viewDir);
+ bNormal.put(viewDir.x).put(viewDir.y).put(viewDir.z);
+
+ // Model space is easy to calculate
+ bTexCoord.put(BASE_CORNERS[j].x * xEx + xEx);
+ bTexCoord.put(BASE_CORNERS[j].y * volumeHeight + volumeHeight);
+ bTexCoord.put(BASE_CORNERS[j].z * zEx + zEx);
+
+ // And so is the scale... since it's always the same
+ bTexCoord2.put(boxScale.x).put(boxScale.y).put(boxScale.z);
+ }
+
+ // Fill in the index buffer
+ for( int j = 0; j < INDEXES_PER_SHADOW; j++ ) {
+ bIndex.put( (short)(BASE_INDEXES[j] + rendered * VERTS_PER_SHADOW) );
+ }
+
+ rendered++;
+ if( rendered >= maxShadows ) {
+ break;
+ }
+ }
+
+ if( rendered > 0 ) {
+ // Need to zero out the left-overs
+ for( int i = rendered; i < maxShadows; i++ ) {
+ for( int j = 0; j < INDEXES_PER_SHADOW; j++ ) {
+ bIndex.put((short)0);
+ }
+ }
+
+ // Update the buffers
+ bPos.rewind();
+ bNormal.rewind();
+ bTexCoord.rewind();
+ bTexCoord2.rewind();
+ bIndex.rewind();
+
+ vbPos.updateData(bPos);
+ vbNormal.updateData(bNormal);
+ vbTexCoord.updateData(bTexCoord);
+ vbTexCoord2.updateData(bTexCoord2);
+ vbIndex.updateData(bIndex);
+
+ shadowGeom.updateGeometricState();
+ renderManager.renderGeometry(shadowGeom);
+ }
+
+ casters.clear();
+ }
+
+ private class CasterComparator implements GeometryComparator {
+
+ private Camera cam;
+ private final Vector3f tempVec = new Vector3f();
+ private final Vector3f tempVec2 = new Vector3f();
+
+ public void setCamera( Camera cam ) {
+ this.cam = cam;
+ }
+
+ public float distanceToCam( Geometry spat ) {
+ if( spat == null ) {
+ return Float.NEGATIVE_INFINITY;
+ }
+
+ if( spat.queueDistance != Float.NEGATIVE_INFINITY ) {
+ return spat.queueDistance;
+ }
+
+ Vector3f camPosition = cam.getLocation();
+ Vector3f viewVector = cam.getDirection(tempVec2);
+ Vector3f spatPosition;
+
+ if( spat.getWorldBound() != null ) {
+ spatPosition = spat.getWorldBound().getCenter();
+ } else {
+ spatPosition = spat.getWorldTranslation();
+ }
+
+ spatPosition.subtract(camPosition, tempVec);
+ spat.queueDistance = tempVec.dot(viewVector);
+
+ return spat.queueDistance;
+ }
+
+ public int compare( Geometry o1, Geometry o2 ) {
+ // Front to back sort
+ float d1 = distanceToCam(o1);
+ float d2 = distanceToCam(o2);
+
+ if( d1 == d2 ) {
+ return 0;
+ } else if( d1 < d2 ) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+ }
+
+
+}
diff --git a/simarboreal/src/main/java/com/simsilica/arboreal/FileActionsState.java b/simarboreal/src/main/java/com/simsilica/arboreal/FileActionsState.java
new file mode 100644
index 0000000..5439902
--- /dev/null
+++ b/simarboreal/src/main/java/com/simsilica/arboreal/FileActionsState.java
@@ -0,0 +1,393 @@
+/*
+ * ${Id}
+ *
+ * Copyright (c) 2014, Simsilica, LLC
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.simsilica.arboreal;
+
+import com.jme3.app.Application;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.SceneGraphVisitorAdapter;
+import com.jme3.scene.Spatial;
+import com.jme3.system.JmeSystem;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
+import com.simsilica.lemur.Button;
+import com.simsilica.lemur.Command;
+import com.simsilica.lemur.Container;
+import com.simsilica.lemur.HAlignment;
+import com.simsilica.lemur.event.BaseAppState;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.SwingUtilities;
+import javax.swing.filechooser.FileFilter;
+import org.progeeks.json.JsonParser;
+import org.progeeks.json.JsonPrinter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Manages the file-related actions.
+ *
+ * @author Paul Speed
+ */
+public class FileActionsState extends BaseAppState {
+
+ static Logger log = LoggerFactory.getLogger(FileActionsState.class);
+
+ private Container buttons;
+
+ public FileActionsState() {
+ }
+
+ @Override
+ protected void initialize( Application app ) {
+
+ buttons = new Container();
+
+ Button saveParms = buttons.addChild(new Button("Save Parms", "glass"));
+ saveParms.addClickCommands(new SaveTreeParameters());
+ saveParms.setTextHAlignment(HAlignment.Center);
+
+ Button loadParms = buttons.addChild(new Button("Load Parms", "glass"), 1);
+ loadParms.addClickCommands(new LoadTreeParameters());
+ loadParms.setTextHAlignment(HAlignment.Center);
+
+ Button saveJ3o = buttons.addChild(new Button("Export j3o", "glass"), 2);
+ saveJ3o.addClickCommands(new SaveJ3o());
+ saveJ3o.setTextHAlignment(HAlignment.Center);
+
+ Button saveTreeAtlas = buttons.addChild(new Button("Save Tree Atlas", "glass"));
+ saveTreeAtlas.addClickCommands(new SaveTreeAtlas());
+ saveTreeAtlas.setTextHAlignment(HAlignment.Center);
+ }
+
+ @Override
+ protected void cleanup( Application app ) {
+ }
+
+ @Override
+ protected void enable() {
+ getState(TreeOptionsState.class).getContents().addChild(buttons);
+ }
+
+ @Override
+ protected void disable() {
+ getState(TreeOptionsState.class).getContents().removeChild(buttons);
+ }
+
+ /**
+ * Filters out the stuff that we probably don't want to... or
+ * really shouldn't be saving in a j3o. We should also remove
+ * the Impostor LODs at least until there is a way to save and/or
+ * embed the impostor image... but I don't right now.
+ */
+ protected Spatial filterClone( Spatial tree ) {
+ Spatial result = tree.deepClone();
+ result.depthFirstTraversal(new SceneGraphVisitorAdapter() {
+ @Override
+ public void visit( Geometry g ) {
+ if( g.getName().startsWith("wire:") ) {
+ g.removeFromParent();
+ }
+ }
+ });
+ return result;
+ }
+
+ private Map lastRoots = new HashMap();
+ protected File chooseFile( final String description, final boolean save, String... extensions ) {
+ //final String ext = (!extension.startsWith(".") ? "." : "") + extension.toLowerCase();
+ final String[] exts = new String[extensions.length];
+ for( int i = 0; i < exts.length; i++ ) {
+ exts[i] = (!extensions[i].startsWith(".") ? "." : "") + extensions[i].toLowerCase();
+ }
+
+ File lastRoot = lastRoots.get(exts[0]);
+ if( lastRoot == null ) {
+ lastRoot = new File(".");
+ }
+
+ log.info("Creating file chooser dialog...");
+ final JFileChooser openDialog = new JFileChooser();
+
+ openDialog.setDialogTitle("Choose Location");
+ if( save ) {
+ openDialog.setDialogType(JFileChooser.SAVE_DIALOG);
+ } else {
+ openDialog.setDialogType(JFileChooser.OPEN_DIALOG);
+ }
+ openDialog.setFileFilter(new FileFilter() {
+
+ @Override
+ public boolean accept( File file ) {
+ if( file.isDirectory() ) {
+ return true;
+ }
+ String s = file.getName().toLowerCase();
+ for( String e : exts ) {
+ if( s.endsWith(e) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+ });
+ openDialog.setCurrentDirectory(lastRoot);
+
+ log.info("Opening file chooser dialog...");
+
+ final int[] dialogResult = new int[1]; //JFileChooser.CANCEL_OPTION ;
+ if( !SwingUtilities.isEventDispatchThread() ) {
+ try {
+ SwingUtilities.invokeAndWait( new Runnable() {
+ @Override
+ public void run() {
+ if( save ) {
+ dialogResult[0] = openDialog.showSaveDialog(null);
+ } else {
+ dialogResult[0] = openDialog.showOpenDialog(null);
+ }
+ }
+ });
+ } catch( Exception e ) {
+ throw new RuntimeException("Error invoking", e);
+ }
+ } else {
+ if( save ) {
+ dialogResult[0] = openDialog.showSaveDialog(null);
+ } else {
+ dialogResult[0] = openDialog.showOpenDialog(null);
+ }
+ }
+
+ if( dialogResult[0] != JFileChooser.APPROVE_OPTION ) {
+ return null;
+ }
+
+ File result = openDialog.getSelectedFile();
+ lastRoots.put(exts[0], result.getParentFile());
+
+ if( save && !result.getName().toLowerCase().endsWith(exts[0]) ) {
+ result = new File(result.getParent(), result.getName() + exts[0]);
+ }
+
+ return result;
+ }
+
+ protected void writeJson( File f, Map map ) throws IOException {
+
+ FileWriter out = new FileWriter(f);
+ try {
+ JsonPrinter json = new JsonPrinter();
+ json.write(map, out);
+ } finally {
+ out.close();
+ }
+ }
+
+ protected Map readJson( File f ) throws IOException {
+ FileReader in = new FileReader(f);
+ try {
+ JsonParser json = new JsonParser();
+ return (Map)json.parse(in);
+ } finally {
+ in.close();
+ }
+ }
+
+ public void saveJ3o( File f ) throws IOException {
+ BinaryExporter exporter = BinaryExporter.getInstance();
+ log.info("Writing:" + f);
+ exporter.save(filterClone(getState(ForestGridState.class).getMainTreeNode()), f);
+ }
+
+ public void saveTreeParameters( File f ) throws IOException {
+ TreeParameters treeParameters = getState(TreeParametersState.class).getTreeParameters();
+ Map map = treeParameters.toMap();
+ log.info("Writing:" + f);
+ writeJson(f, map);
+ }
+
+ public void loadTreeParameters( File f ) throws IOException {
+ Map map = readJson(f);
+ TreeParameters treeParameters = getState(TreeParametersState.class).getTreeParameters();
+ treeParameters.fromMap(map);
+ getState(TreeParametersState.class).refreshTreePanels();
+ getState(ForestGridState.class).rebuild();
+ }
+
+ public void savePng( File f, Image img ) throws IOException {
+ OutputStream out = new FileOutputStream(f);
+ try {
+ JmeSystem.writeImageFile(out, "png", img.getData(0), img.getWidth(), img.getHeight());
+ } finally {
+ out.close();
+ }
+ }
+
+ public void saveTreeAtlas( File f ) throws IOException {
+
+ Image diffuse = getState(AtlasGeneratorState.class).getDiffuseMap();
+ savePng(f, diffuse);
+
+ String normalName = f.getName();
+ if( normalName.toLowerCase().endsWith(".png") ) {
+ normalName = normalName.substring(0, normalName.length() - ".png".length());
+ }
+ f = new File(f.getParentFile(), normalName + "-normals.png");
+
+ Image normal = getState(AtlasGeneratorState.class).getNormalMap();
+ savePng(f, normal);
+ }
+
+ private class SaveJ3o implements Command