Weiter am Menü und an der Vollbildproblematik gearbeitet

This commit is contained in:
2026-07-12 22:27:22 +02:00
parent f109241e1f
commit 6cf8f52faf
41 changed files with 1848 additions and 748 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ run/
# Spielstände
saves/
# Claude Code interne Worktrees
.claude/worktrees/

View File

@@ -30,10 +30,15 @@ void main() {
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;
// Mindest-Stärke damit bei wenig Wind noch Bewegung sichtbar ist
float effectiveStrength = max(m_WindStrength, 0.05);
// Quadratische Gewichtung: Spitze bewegt sich mehr als Basis
float bend = sway * m_WindStrength * inTexCoord.y * inTexCoord.y;
float bend = sway * effectiveStrength * inTexCoord.y * inTexCoord.y;
pos.x += bend;
pos.z += bend * 0.3;
// Y-Kompression: Halm neigt sich statt zu strecken → verhindert visuelles Breiterwerden
pos.y -= bend * bend * 0.5;
}
texCoord = inTexCoord;

View File

@@ -34,10 +34,15 @@ void main() {
float sway = sin(t * 2.1 + wavePhase * 0.10 + randPhase) * 0.6
+ sin(t * 1.4 + wavePhase * 0.06 + randPhase * 0.73) * 0.4;
// Mindest-Stärke damit bei wenig Wind noch Bewegung sichtbar ist
float effectiveStrength = max(m_WindStrength, 0.05);
// Quadratische Gewichtung: Spitze biegt sich mehr als Basis
float bend = sway * m_WindStrength * wf * wf;
float bend = sway * effectiveStrength * wf * wf;
pos.x += windN.x * bend;
pos.z += windN.y * bend;
// Y-Kompression: Halm neigt sich statt zu strecken → verhindert visuelles Breiterwerden
pos.y -= bend * bend * 0.5;
}
varColor = inColor;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

View File

@@ -2,12 +2,12 @@
"chapter": 0,
"level": 0,
"xp": 0,
"currentHp": 0,
"currentStamina": 0,
"currentMana": 0,
"maxHp": 0,
"maxStamina": 0,
"maxMana": 0,
"currentHp": 100,
"currentStamina": 100,
"currentMana": 100,
"maxHp": 100,
"maxStamina": 100,
"maxMana": 100,
"openHpRegeneration": 0,
"openManaRegeneration": 0,
"openStaminaRegeneration": 0,

View File

@@ -7330,6 +7330,11 @@ public class EditorApp extends Application {
private void startGameProcess() {
final boolean isNewGame = pendingNewGame;
pendingNewGame = false;
// Editor minimieren, damit das GLFW-Vollbild-Fenster keinen Focus-Konkurrenten hat.
// GLFW_AUTO_ICONIFY würde das Vollbild-Fenster sonst sofort minimieren, weil der
// Editor beim Starten noch den Fokus hat. Der JVM-Start des Spiels dauert mehrere
// Sekunden das Fenster ist längst minimiert, bevor GLFW das Fenster anlegt.
Platform.runLater(() -> { if (primaryStage != null) primaryStage.setIconified(true); });
new Thread(() -> {
try {
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
@@ -7387,9 +7392,10 @@ public class EditorApp extends Application {
consoleBuffer.offer(line);
}
}
// Spiel beendet → Buttons freigeben
// Spiel beendet → Buttons freigeben und Editor wiederherstellen
consoleBuffer.offer("--- Spiel beendet ---");
Platform.runLater(() -> {
if (primaryStage != null) primaryStage.setIconified(false);
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); }
});
@@ -8863,7 +8869,7 @@ public class EditorApp extends Application {
Spinner<Double> spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0);
Spinner<Double> spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0);
Spinner<Double> spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0);
for (Spinner<Double> sp : new Spinner[]{spTX, spTY, spTZ, spRX, spRY, spRZ}) {
for (Spinner<Double> sp : java.util.List.of(spTX, spTY, spTZ, spRX, spRY, spRZ)) {
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
}

View File

@@ -262,6 +262,7 @@ public class JmeEditorApp extends SimpleApplication {
// ── Framebuffer-Verwaltung ────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private void buildFrameBuffer(int w, int h, WritableImage image) {
Texture2D colorTex = new Texture2D(w, h, Image.Format.RGBA8);
FrameBuffer fb = new FrameBuffer(w, h, 1);

View File

@@ -37,6 +37,7 @@ public class LegacyAssetRedirectLocator implements AssetLocator {
}
@Override
@SuppressWarnings("rawtypes")
public AssetInfo locate(AssetManager manager, AssetKey key) {
String name = key.getName();
for (var entry : PREFIXES.entrySet()) {
@@ -55,6 +56,7 @@ public class LegacyAssetRedirectLocator implements AssetLocator {
return null;
}
@SuppressWarnings("rawtypes")
private AssetInfo tryFile(AssetManager manager, AssetKey key, String rel) {
Path p = root.resolve(rel);
if (!Files.exists(p)) return null;

View File

@@ -454,6 +454,7 @@ public class AnimPreviewState extends BaseAppState {
input.skeletalPaths.set(Collections.unmodifiableSet(result));
}
@SuppressWarnings("deprecation")
private boolean hasSkeleton(Spatial s) {
if (s.getControl(AnimComposer.class) != null) return true;
// Fallback auf altes AnimControl (Legacy-Modelle)
@@ -775,6 +776,7 @@ public class AnimPreviewState extends BaseAppState {
// ── Framebuffer ───────────────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private FrameBuffer buildFrameBuffer(int w, int h) {
FrameBuffer fb = new FrameBuffer(w, h, 1);
fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8));
@@ -1023,6 +1025,7 @@ public class AnimPreviewState extends BaseAppState {
* Hält jeden Root-Joint an seiner lokalen Bind-Pose-Transform → SC-Matrix = I → T-Pose.
* Gibt null zurück wenn das Armature keine Root-Joints hat.
*/
@SuppressWarnings("rawtypes")
private static AnimClip buildTPoseClip(com.jme3.anim.Armature armature) {
int jointCount = armature.getJointCount();
if (jointCount == 0) return null;

View File

@@ -383,6 +383,7 @@ public class EzTreeState extends BaseAppState {
// ── Phase 2: Impostor-Capture (4-Richtungen) ─────────────────────────────
@SuppressWarnings("deprecation")
private void startCapturePass(int pass, Node treeNode, BoundingBox bb) {
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);

View File

@@ -86,7 +86,7 @@ public class GrassVertexState extends BaseAppState {
private Material[] seedMaterials = new Material[0];
private Material seedStalkMaterial;
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT];

View File

@@ -155,6 +155,7 @@ public class PalmGeneratorState extends BaseAppState {
// ── Phase 2: Capture ─────────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private void startCapturePass(int pass) {
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);

View File

@@ -72,7 +72,7 @@ public class PlacedObjectState extends BaseAppState {
private Node grassNode;
private final Map<Integer, Material> slotMaterials = new LinkedHashMap<>();
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
private final List<GrassTuft>[] chunkTufts = new List[CHUNK_COUNT];
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT];

View File

@@ -1368,6 +1368,7 @@ public class SceneObjectState extends BaseAppState {
// ── Modell-Konvertierung ──────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private void convertModel(SharedInput.ModelConvertRequest req) {
setStatus("Konvertiere " + req.assetPath() + "");
try {

View File

@@ -495,7 +495,7 @@ public class SculptedMeshEditorState extends BaseAppState {
}
private static int[][] buildNeighbors(int wCount, int[] indices) {
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
Set<Integer>[] sets = new Set[wCount];
for (int i = 0; i < wCount; i++) sets[i] = new HashSet<>();
for (int i = 0; i < indices.length; i += 3) {

View File

@@ -57,7 +57,7 @@ public class StoneEditorState extends BaseAppState {
private Node rootNode;
private Node stoneRoot;
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
private final List<PlacedStone>[] chunkStones = new List[CHUNK_COUNT];
/** Pro Chunk: Node mit LOD0-Geometrien. */
private final Node[] lod0Nodes = new Node[CHUNK_COUNT];

View File

@@ -23,6 +23,7 @@ import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import com.jme3.util.SkyFactory;
import de.blight.common.EmitterIO;
@@ -492,7 +493,7 @@ public class TerrainEditorState extends BaseAppState {
splatBuf.put(splatR[i]).put(splatG[i]).put(splatB[i]).put(splatA[i]);
}
splatBuf.flip();
splatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, splatBuf);
splatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, splatBuf, ColorSpace.Linear);
splatTex = new Texture2D(splatImage);
splatTex.setWrap(Texture.WrapMode.EdgeClamp);
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
@@ -503,7 +504,7 @@ public class TerrainEditorState extends BaseAppState {
upperSplatBuf.put(upperSplatR[i]).put(upperSplatG[i]).put(upperSplatB[i]).put(upperSplatA[i]);
}
upperSplatBuf.flip();
upperSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, upperSplatBuf);
upperSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, upperSplatBuf, ColorSpace.Linear);
upperSplatTex = new Texture2D(upperSplatImage);
upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
@@ -514,7 +515,7 @@ public class TerrainEditorState extends BaseAppState {
thirdSplatBuf.put(thirdSplatR[i]).put(thirdSplatG[i]).put(thirdSplatB[i]).put(thirdSplatA[i]);
}
thirdSplatBuf.flip();
thirdSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, thirdSplatBuf);
thirdSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, thirdSplatBuf, ColorSpace.Linear);
thirdSplatTex = new Texture2D(thirdSplatImage);
thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
@@ -617,13 +618,13 @@ public class TerrainEditorState extends BaseAppState {
if (path != null && !path.isEmpty()) {
try {
ByteBuffer buf = loadTextureToRGBA8(path, size);
images.add(new Image(Image.Format.RGBA8, size, size, buf));
images.add(new Image(Image.Format.RGBA8, size, size, buf, ColorSpace.Linear));
continue;
} catch (Exception e) {
log.warn("[TerrainArray] Slot {} nicht ladbar ({}): {}", i, path, e.getMessage());
}
}
images.add(new Image(Image.Format.RGBA8, size, size, solidColorLayer(fb, size)));
images.add(new Image(Image.Format.RGBA8, size, size, solidColorLayer(fb, size), ColorSpace.Linear));
}
com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images);
@@ -1769,6 +1770,7 @@ public class TerrainEditorState extends BaseAppState {
return g;
}
@SuppressWarnings("deprecation")
private void addGizmoLabel(Node parent, Vector3f pos, String text, ColorRGBA color) {
try {
com.jme3.font.BitmapFont font = assets.loadFont("Interface/Fonts/Default.fnt");

View File

@@ -159,6 +159,7 @@ public final class ThumbnailRenderer {
// ── Intern ───────────────────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private static byte[] renderToFramebuffer(Node scene, Camera cam,
RenderManager rm, Renderer renderer) {
Texture2D colorTex = new Texture2D(SIZE, SIZE, Image.Format.RGBA8);

View File

@@ -224,6 +224,7 @@ public class TreeGeneratorState extends BaseAppState {
// ── Framebuffer-Helpers ───────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private FrameBuffer buildFrameBuffer(int w, int h) {
FrameBuffer fb = new FrameBuffer(w, h, 1);
fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8));
@@ -429,7 +430,7 @@ public class TreeGeneratorState extends BaseAppState {
return atlas;
}
/** Startet einen einzelnen Capture-Durchlauf für die gegebene Richtung (Pass 0..3). */
@SuppressWarnings("deprecation")
private void startCapturePass(int pass) {
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);

View File

@@ -1565,7 +1565,8 @@ public class VoxelEditorState extends BaseAppState {
buf.put((byte) rgb[0]).put((byte) rgb[1]).put((byte) rgb[2]).put((byte) 255);
buf.flip();
com.jme3.texture.Texture2D tex = new com.jme3.texture.Texture2D(
new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf));
new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf,
com.jme3.texture.image.ColorSpace.Linear));
tex.setWrap(Texture.WrapMode.Repeat);
return tex;
}

View File

@@ -71,7 +71,9 @@ public class LocalizationEditorView extends BorderPane {
table.setEditable(true);
table.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
@SuppressWarnings("deprecation")
var policy = TableView.CONSTRAINED_RESIZE_POLICY;
table.setColumnResizePolicy(policy);
TableColumn<String[], String> keyCol = new TableColumn<>("Schlüssel");
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));

View File

@@ -18,7 +18,14 @@ import de.blight.game.console.JmeConsole;
import de.blight.game.scene.WorldScene;
import de.blight.lang.TextResolver;
import org.lwjgl.PointerBuffer;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.system.MemoryStack;
import com.jme3.system.lwjgl.LwjglWindow;
import javax.imageio.ImageIO;
import java.nio.IntBuffer;
import java.util.Locale;
import javax.swing.*;
import java.awt.*;
@@ -47,6 +54,11 @@ public class BlightGame extends SimpleApplication {
/** Routed durch onClose-Callbacks, damit Config/Grafik-Screen zurück zum richtigen Menü springen. */
private Runnable screenCloseTarget;
// Vollbild-Watchdog: erkennt, wenn das Fenster unerwartet die Auflösung wechselt
// (z.B. GNOME Shell de-fullscreent OverrideRedirect-Fenster bei Monitorfokus-Wechsel)
private long startTimeMs = 0;
private long lastFullscreenFix = 0;
private JWindow splashWindow;
// ── Splash-Status (von JME-Thread geschrieben, EDT liest) ─────────────────
@@ -69,11 +81,41 @@ public class BlightGame extends SimpleApplication {
try {
settings.setIcons(new Object[]{ImageIO.read(BlightGame.class.getResourceAsStream("/icon.png"))});
} catch (IOException | NullPointerException ignored) {}
settings.setResolution(gs.width, gs.height);
int targetW = gs.width, targetH = gs.height;
if (gs.fullscreen) {
// GNOME Shell lehnt Nicht-Nativ-Auflösungen bei GLFW-Vollbild (OverrideRedirect) ab.
// Nicht-native Auflösungen führen zu WM-Dekorationen oder falschem Monitor.
// → Immer native Auflösung des AWT-Primärmonitors verwenden.
try {
Rectangle screen = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.getBounds();
targetW = screen.width;
targetH = screen.height;
} catch (Exception ignored) {}
}
settings.setResolution(targetW, targetH);
settings.setFullscreen(gs.fullscreen);
settings.setBitsPerPixel(32);
settings.setVSync(gs.vsync);
settings.setSamples(gs.samples);
// X11 erzwingen: auf Systemen mit Wayland-Libs läuft GLFW sonst im Wayland-Modus,
// was libdecor-Fensterrahmen auch im Vollbild aktiviert.
settings.setX11PlatformPreferred(true);
// Splash vor GLFW-Fenstererstellung schließen: verhindert auto-iconify
// (JWindow kann X11-Fokus zurückbekommen → GLFW-Vollbild sofort minimiert)
if (gs.fullscreen && app.splashWindow != null) {
try {
SwingUtilities.invokeAndWait(() -> {
app.splashWindow.setVisible(false);
app.splashWindow.dispose();
});
} catch (Exception ignored) {}
app.splashWindow = null;
}
status("Initialisiere Renderer...");
app.setSettings(settings);
@@ -143,6 +185,16 @@ public class BlightGame extends SimpleApplication {
keyBindings = KeyBindingStore.load();
graphicsSettings = GraphicsStore.load();
audioSettings = AudioSettingsStore.load();
startTimeMs = System.currentTimeMillis();
if (graphicsSettings.fullscreen) {
// Tatsächliche Cam-Auflösung als Watchdog-Baseline (main() nutzt native Monitorgröße)
graphicsSettings.width = cam.getWidth();
graphicsSettings.height = cam.getHeight();
log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={}",
cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen);
fixFullscreenMonitor();
}
stateManager.attach(new AudioSettingsState(audioSettings));
status("Lade Spielstand...");
@@ -340,6 +392,21 @@ public class BlightGame extends SimpleApplication {
gameReady = true;
status("Bereit");
}
// Vollbild-Watchdog: Wenn GNOME Shell oder der WM das Fenster unerwartet aus dem
// Vollbild-Modus holt (erkennbar an falscher Auflösung), sofort wiederherstellen.
// 5s Anlaufzeit überspringen (Initialisierungs-Resize-Events), 3s Cooldown.
if (graphicsSettings != null && graphicsSettings.fullscreen) {
long now = System.currentTimeMillis();
if (now - startTimeMs > 5_000
&& (cam.getWidth() != graphicsSettings.width
|| cam.getHeight() != graphicsSettings.height)
&& now - lastFullscreenFix > 3_000) {
log.warn("[Grafik] Vollbild verloren ({}×{} statt {}×{}), starte Wiederherstellung",
cam.getWidth(), cam.getHeight(), graphicsSettings.width, graphicsSettings.height);
lastFullscreenFix = now;
fixFullscreenMonitor();
}
}
}
// ── Konsolen-Befehle ─────────────────────────────────────────────────────
@@ -388,7 +455,7 @@ public class BlightGame extends SimpleApplication {
}
});
console.registerCommand("weather", args -> {
console.registerCommand("weather", (args) -> {
de.blight.game.state.WeatherState ws =
stateManager.getState(de.blight.game.state.WeatherState.class);
if (ws == null) return "Wettersystem nicht aktiv";
@@ -405,4 +472,81 @@ public class BlightGame extends SimpleApplication {
}
});
}
// Vollbild sicherstellen: GLFW ist hier bereits durch JME3 initialisiert.
// Problem: GLFW_AUTO_ICONIFY (default true) minimiert das Vollbild-Fenster sofort,
// wenn beim Start ein anderes Fenster (z.B. Editor) noch den Fokus hat.
// Der Editor minimiert sich inzwischen selbst vor dem Spielstart dieser Fix greift
// dennoch als Fallback und korrigiert außerdem einen falschen Monitor-Index.
private void fixFullscreenMonitor() {
if (!(context instanceof LwjglWindow)) {
log.warn("[Grafik] Kein LwjglWindow-Kontext, Vollbild-Fix übersprungen");
return;
}
long win = ((LwjglWindow) context).getWindowHandle();
if (win == 0L) {
log.warn("[Grafik] Window-Handle ist 0, Vollbild-Fix übersprungen");
return;
}
try {
// Auto-Iconify deaktivieren
GLFW.glfwSetWindowAttrib(win, GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_FALSE);
// Ggf. iconifiziertes Fenster wiederherstellen
GLFW.glfwRestoreWindow(win);
// Ist das Fenster bereits in GLFW-Vollbild (OverrideRedirect)?
long currentMonitor = GLFW.glfwGetWindowMonitor(win);
// AWT-Primary-Monitor mit GLFW-Monitoren abgleichen
Rectangle awt = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.getBounds();
PointerBuffer mons = GLFW.glfwGetMonitors();
if (mons == null || mons.limit() == 0) {
log.warn("[Grafik] glfwGetMonitors() liefert nichts");
GLFW.glfwFocusWindow(win);
return;
}
long target = GLFW.glfwGetPrimaryMonitor();
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer mx = stack.mallocInt(1);
IntBuffer my = stack.mallocInt(1);
for (int i = 0; i < mons.limit(); i++) {
GLFW.glfwGetMonitorPos(mons.get(i), mx, my);
log.info("[Grafik] GLFW-Monitor {}: pos=({},{}) | AWT-Primary: ({},{}) | aktuellerMonitor={}",
i, mx.get(0), my.get(0), awt.x, awt.y,
(currentMonitor == mons.get(i)) ? "JA" : "nein");
if (mx.get(0) == awt.x && my.get(0) == awt.y) {
target = mons.get(i);
}
}
}
if (currentMonitor == target) {
// Bereits korrekt im Vollbild auf dem richtigen Monitor kein Monitor-Wechsel,
// da glfwSetWindowMonitor sonst den OverrideRedirect-Zustand stören kann.
log.info("[Grafik] Fenster ist bereits Vollbild auf dem Ziel-Monitor nur Fokus setzen");
GLFW.glfwFocusWindow(win);
return;
}
// Falscher Monitor oder fensterbasierter Modus → Vollbild erzwingen
GLFWVidMode vm = GLFW.glfwGetVideoMode(target);
if (vm == null) {
log.warn("[Grafik] Kein VideoMode für Ziel-Monitor");
return;
}
log.info("[Grafik] Setze Vollbild: {}×{}@{}Hz (war: Monitor-Handle={})",
vm.width(), vm.height(), vm.refreshRate(), currentMonitor);
GLFW.glfwSetWindowMonitor(win, target, 0, 0, vm.width(), vm.height(), vm.refreshRate());
GLFW.glfwFocusWindow(win);
} catch (Throwable e) {
log.warn("[Grafik] Vollbild-Fix fehlgeschlagen: {}", e.getMessage(), e);
}
}
}

View File

@@ -251,6 +251,7 @@ public class AnimationLibrary extends BaseAppState {
}
}
@SuppressWarnings("rawtypes")
private static AnimClip extractSubClip(AnimClip source, String name, float startSec, float endSec) {
List<AnimTrack<?>> newTracks = new ArrayList<>();
for (AnimTrack<?> track : source.getTracks()) {
@@ -350,6 +351,7 @@ public class AnimationLibrary extends BaseAppState {
* bleibt frei für Sitz/Stand-Clips (leichte Neigungsbewegung)
* Local Z → Höhe → IMMER frei lassen (Charakter-Höhe und Setz-Bewegung erhalten)
*/
@SuppressWarnings("rawtypes")
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
if (clip == null || armature == null) return clip;

View File

@@ -56,6 +56,7 @@ public final class RetargetingSystem {
return retarget(sourceClip, sourceArmature, targetArmature, MS_CORRECTIONS);
}
@SuppressWarnings("rawtypes")
public static AnimClip retarget(AnimClip sourceClip,
Armature sourceArmature,
Armature targetArmature,
@@ -413,6 +414,7 @@ public final class RetargetingSystem {
return true;
}
@SuppressWarnings("rawtypes")
private static AnimClip redirectTracks(AnimClip sourceClip, Armature targetArmature) {
List<AnimTrack<?>> newTracks = new ArrayList<>();
for (AnimTrack<?> t : sourceClip.getTracks()) {
@@ -462,6 +464,7 @@ public final class RetargetingSystem {
*
* Erstellt neue TransformTrack-Objekte, damit BinaryExporter die Änderung serialisiert.
*/
@SuppressWarnings("rawtypes")
public static AnimClip stripRootXZTranslations(AnimClip clip, Armature armature) {
if (clip == null || armature == null) return clip;
log.info("[Strip] Clip '{}' {} Tracks", clip.getName(), clip.getTracks().length);

View File

@@ -1,59 +1,26 @@
package de.blight.game.config;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.*;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import de.blight.lang.TextResolver;
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;
import de.blight.lang.TextResolver;
public class ConfigScreen extends MenuScreen implements RawInputListener {
public class ConfigScreen extends BaseAppState implements RawInputListener {
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 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 static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
private KeyBindings liveBindings;
private KeyBindings editCopy;
private Runnable onSave;
private Runnable onClose;
private Node panel;
private Node bgLayer;
private Node canvasNode;
private List<Row> rows = new ArrayList<>();
private int waitingRow = -1;
private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
private float cancelBtnX, cancelBtnY;
private Runnable onSave;
private Runnable onClose;
private static class Row {
String field;
@@ -62,92 +29,68 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
float x, y, w, h;
}
private final List<Row> rows = new ArrayList<>();
private int waitingRow = -1;
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 boolean isWaiting() { return waitingRow >= 0; }
public void setOnClose(Runnable r) { this.onClose = r; }
public void cancelWaiting() {
if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
}
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
}
// ── MenuScreen ────────────────────────────────────────────────────────────
@Override
protected void onEnable() {
protected void onEnableExtras() {
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 (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
panel = null;
protected void onDisableExtras() {
rows.clear();
waitingRow = -1;
app.getInputManager().removeRawInputListener(this);
app.getInputManager().deleteMapping("_CfgClick");
app.getInputManager().setCursorVisible(false);
}
@Override protected void cleanup(Application app) {}
@Override
protected void buildUI() {
initPanel(t("menu.controls.title"));
private void buildUI() {
float sw = MenuCanvas.REF_W;
float sh = MenuCanvas.REF_H;
bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera());
canvasNode = MenuCanvas.createCanvas(app.getCamera());
guiNode.attachChild(bgLayer);
guiNode.attachChild(canvasNode);
panel = new Node("cfg-panel");
float pw = 720, ph = 440;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
BitmapText title = text(t("menu.controls.title"), 20, COL_TEXT);
centerText(title, px, py + ph - 40, pw);
panel.attachChild(title);
BitmapText hint = text(t("menu.controls.hint"), 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
centerText(hint, px, py + ph - 70, pw);
BitmapText hint = crispText(t("menu.controls.hint"), UiTheme.FONT_SMALL,
new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
hint.setLocalTranslation(
CONT_X + (CONT_W - hint.getLineWidth() / getUiScale()) / 2f,
CONT_Y + CONT_H - HDR_H - 14f, -17f);
panel.attachChild(hint);
float rowX = px + 30;
float keyX = px + pw - 220;
float rowW = 180;
float rowH = 36;
float startY = py + ph - 110;
float stepY = 48;
float rowLblX = CONT_X + 30f;
float keyX = CONT_X + CONT_W - 220f;
float rowW = 180f;
float rowH = 36f;
float startY = CONT_Y + CONT_H - HDR_H - 50f;
float stepY = 48f;
for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
String[] entry = KeyBindings.ENTRIES[i];
float ry = startY - i * stepY;
BitmapText lbl = text(t("key." + entry[0]), 16, COL_TEXT);
lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
BitmapText lbl = crispText(t("key." + entry[0]), UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
lbl.setLocalTranslation(rowLblX, ry + rowH - 4f, 0f);
panel.attachChild(lbl);
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0f);
BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
BitmapText kt = crispText(KeyNames.of(editCopy.get(entry[0])),
UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
kt.setLocalTranslation(keyX + 10f, ry + rowH - 4f, 1f);
panel.attachChild(kt);
Row row = new Row();
@@ -158,61 +101,34 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
rows.add(row);
}
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;
float btnW = 160f, btnH = 42f;
float btnY = CONT_Y + 22f;
float saveX = CONT_X + CONT_W / 2f - btnW - 10f;
float cancelX = CONT_X + CONT_W / 2f + 10f;
panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(saveBtnX, saveBtnY, btnW, btnH, 0));
BitmapText saveLabel = text(t("menu.controls.btn.save"), 16, COL_TEXT);
centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
panel.attachChild(saveLabel);
panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelBtnX, cancelBtnY, btnW, btnH, 0));
BitmapText cancelLabel = text(t("menu.controls.btn.cancel"), 16, COL_TEXT);
centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
panel.attachChild(cancelLabel);
canvasNode.attachChild(panel);
addButton(panel, t("menu.controls.btn.save"), saveX, btnY, btnW, btnH, BtnStyle.SAVE, this::doSave);
addButton(panel, t("menu.controls.btn.cancel"), cancelX, btnY, btnW, btnH, BtnStyle.QUIT, this::doClose);
}
private final ActionListener clickListener = (name, isPressed, tpf) -> {
if (!isPressed) return;
float[] v = toVirtual(app.getInputManager().getCursorPosition());
@Override
protected void onScreenClick(float[] v) {
for (int i = 0; i < rows.size(); i++) {
Row r = rows.get(i);
if (hits(v, r.x, r.y, r.w, r.h)) {
if (v[0] >= r.x && v[0] <= r.x + r.w && v[1] >= r.y && v[1] <= r.y + r.h) {
waitingRow = i;
r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
r.keyText.setText("...");
return;
}
}
}
if (hits(v, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
liveBindings.copyFrom(editCopy);
KeyBindingStore.save(liveBindings);
if (onSave != null) onSave.run();
setEnabled(false);
if (onClose != null) onClose.run();
return;
}
if (hits(v, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
setEnabled(false);
if (onClose != null) onClose.run();
}
};
// ── RawInputListener ──────────────────────────────────────────────────────
@Override
public void onKeyEvent(KeyInputEvent evt) {
if (!evt.isPressed() || waitingRow < 0) return;
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return;
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return;
Row r = rows.get(waitingRow);
editCopy.set(r.field, evt.getKeyCode());
@@ -221,20 +137,6 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
waitingRow = -1;
}
private void resetRowColor(int idx) {
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
}
private float[] toVirtual(Vector2f screen) {
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
app.getCamera().getHeight() / MenuCanvas.REF_H);
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale };
}
private static String t(String id) { return TextResolver.get().resolveId(id); }
@Override public void beginInput() {}
@Override public void endInput() {}
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
@@ -243,33 +145,23 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
@Override public void onTouchEvent(TouchEvent evt) {}
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 ───────────────────────────────────────────────────────────────
private void doSave() {
liveBindings.copyFrom(editCopy);
KeyBindingStore.save(liveBindings);
if (onSave != null) onSave.run();
doClose();
}
private BitmapText text(String content, int size, ColorRGBA color) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setColor(color);
t.setText(content);
return t;
private void doClose() {
setEnabled(false);
if (onClose != null) onClose.run();
}
private void centerText(BitmapText t, float x, float y, float width) {
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
private void resetRowColor(int idx) {
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
}
private boolean hits(float[] v, float x, float y, float w, float h) {
return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h;
}
private static String t(String id) { return TextResolver.get().resolveId(id); }
}

View File

@@ -1,28 +1,12 @@
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;
import de.blight.lang.TextResolver;
public class GraphicsScreen extends BaseAppState {
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);
public class GraphicsScreen extends MenuScreen {
private static final int[][] RESOLUTIONS = {
{1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
@@ -33,32 +17,21 @@ public class GraphicsScreen extends BaseAppState {
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 Node bgLayer;
private Node canvasNode;
private static final int ROW_COUNT = 4;
private final GraphicsSettings live;
private GraphicsSettings edit;
private final Runnable onClose;
private GraphicsSettings edit;
private final Runnable onClose;
private int resIdx;
private int samplesIdx;
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;
private final float arrW = 30f;
private final float cellH = 36f;
private final float[] cellX = new float[ROW_COUNT];
private final float[] cellY = new float[ROW_COUNT];
private final float[] cellW = new float[ROW_COUNT];
private final BitmapText[] valTexts = new BitmapText[ROW_COUNT];
public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
this.live = live;
@@ -66,19 +39,12 @@ public class GraphicsScreen extends BaseAppState {
}
@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() {
protected void onEnableExtras() {
edit = new GraphicsSettings();
edit.width = live.width; edit.height = live.height;
edit.fullscreen = live.fullscreen;
edit.vsync = live.vsync;
edit.samples = live.samples;
edit.vsync = live.vsync;
edit.samples = live.samples;
resIdx = 0;
for (int i = 0; i < RESOLUTIONS.length; i++) {
@@ -90,103 +56,56 @@ public class GraphicsScreen extends BaseAppState {
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 (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
panel = null;
app.getInputManager().deleteMapping("_GfxClick");
app.getInputManager().setCursorVisible(false);
}
protected void buildUI() {
initPanel(t("menu.graphics.title"));
@Override protected void cleanup(Application app) {}
private void buildUI() {
float sw = MenuCanvas.REF_W;
float sh = MenuCanvas.REF_H;
bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera());
canvasNode = MenuCanvas.createCanvas(app.getCamera());
guiNode.attachChild(bgLayer);
guiNode.attachChild(canvasNode);
panel = new Node("gfx-panel");
float pw = 640, ph = 400;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
BitmapText title = txt(t("menu.graphics.title"), 20, COL_TEXT);
centerText(title, px, py + ph - 42, pw);
panel.attachChild(title);
String[] labelKeys = {
String[] lblKeys = {
"menu.graphics.row.resolution",
"menu.graphics.row.fullscreen",
"menu.graphics.row.vsync",
"menu.graphics.row.aa"
};
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 lblX = CONT_X + 30f;
float vx = CONT_X + CONT_W - 270f;
float vw = 190f;
float startY = CONT_Y + CONT_H - HDR_H - 58f;
float step = 60f;
for (int i = 0; i < ROW_COUNT; i++) {
final int row = i;
float ry = startY - i * step;
BitmapText lbl = txt(t(labelKeys[i]), 16, COL_TEXT);
lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
BitmapText lbl = crispText(t(lblKeys[i]), UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
lbl.setLocalTranslation(lblX, ry + cellH - 4f, 0f);
panel.attachChild(lbl);
panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx - arrW - 6, ry, arrW, cellH, 0));
BitmapText lt = txt("<", 16, COL_TEXT);
lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
panel.attachChild(lt);
addButton(panel, "<", vx - arrW - 6f, ry, arrW, cellH,
BtnStyle.ARROW, () -> cycleRow(row, -1));
panel.attachChild(NinePatch.button(app.getAssetManager()).build(vx, ry, vw, cellH, 0));
panel.attachChild(NinePatch.button(assets).build(vx, ry, vw, cellH, 0f));
panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx + vw + 6, ry, arrW, cellH, 0));
BitmapText rt = txt(">", 16, COL_TEXT);
rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
panel.attachChild(rt);
addButton(panel, ">", vx + vw + 6f, ry, arrW, cellH,
BtnStyle.ARROW, () -> cycleRow(row, +1));
BitmapText vt = txt("", 16, COL_TEXT_VAL);
BitmapText vt = crispText("", UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
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);
for (int i = 0; i < ROW_COUNT; 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;
panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(okX, okY, bw, bh, 0));
BitmapText okLbl = txt(t("menu.graphics.btn.apply"), 16, COL_TEXT);
centerText(okLbl, okX, okY + bh - 10, bw);
panel.attachChild(okLbl);
panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelX, cancelY, bw, bh, 0));
BitmapText cancelLbl = txt(t("menu.graphics.btn.cancel"), 16, COL_TEXT);
centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
panel.attachChild(cancelLbl);
canvasNode.attachChild(panel);
float bw = 160f, bh = 42f;
float btnY = CONT_Y + 22f;
float saveX = CONT_X + CONT_W / 2f - bw - 10f;
float canX = CONT_X + CONT_W / 2f + 10f;
addButton(panel, t("menu.graphics.btn.apply"), saveX, btnY, bw, bh, BtnStyle.SAVE, this::applyAndSave);
addButton(panel, t("menu.graphics.btn.cancel"), canX, btnY, bw, bh, BtnStyle.QUIT, this::close);
}
private void refreshText(int row) {
@@ -195,31 +114,19 @@ public class GraphicsScreen extends BaseAppState {
case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_AA -> SAMPLES[samplesIdx] == 0
? t("menu.graphics.val.off")
: SAMPLES[samplesIdx] + "x MSAA";
? t("menu.graphics.val.off") : SAMPLES[samplesIdx] + "x MSAA";
default -> "";
};
BitmapText vt = valTexts[row];
vt.setText(val);
float tw = vt.getLineWidth() / getUiScale();
vt.setLocalTranslation(
cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
cellY[row] + cellH - 8,
1
cellX[row] + (cellW[row] - tw) / 2f,
cellY[row] + cellH - 4f,
1f
);
}
private final ActionListener clickListener = (name, isPressed, tpf) -> {
if (!isPressed) return;
float[] v = toVirtual(app.getInputManager().getCursorPosition());
for (int i = 0; i < 4; i++) {
if (hits(v, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; }
if (hits(v, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; }
}
if (hits(v, okX, okY, okW, okH)) { applyAndSave(); return; }
if (hits(v, cancelX, cancelY, okW, okH)) { close(); }
};
private void cycleRow(int row, int dir) {
switch (row) {
case ROW_RES:
@@ -227,12 +134,8 @@ public class GraphicsScreen extends BaseAppState {
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_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];
@@ -244,9 +147,8 @@ public class GraphicsScreen extends BaseAppState {
private void applyAndSave() {
live.width = edit.width; live.height = edit.height;
live.fullscreen = edit.fullscreen;
live.vsync = edit.vsync;
live.samples = edit.samples;
live.vsync = edit.vsync;
live.samples = edit.samples;
GraphicsStore.save(live);
AppSettings s = app.getContext().getSettings();
@@ -266,27 +168,5 @@ public class GraphicsScreen extends BaseAppState {
if (onClose != null) onClose.run();
}
private float[] toVirtual(Vector2f screen) {
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
app.getCamera().getHeight() / MenuCanvas.REF_H);
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale };
}
private static String t(String id) { return TextResolver.get().resolveId(id); }
private BitmapText txt(String s, int size, ColorRGBA color) {
BitmapText t = new BitmapText(font);
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(float[] v, float x, float y, float w, float h) {
return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h;
}
}

View File

@@ -13,8 +13,12 @@ public class KeyBindings {
public int sprint = KeyInput.KEY_LSHIFT;
public int walk = KeyInput.KEY_LMENU;
public int interact = KeyInput.KEY_E;
public int turnLeft = KeyInput.KEY_Q;
public int turnRight = KeyInput.KEY_E;
public int inventory = KeyInput.KEY_I;
public int quicksave = KeyInput.KEY_F5;
public int character = KeyInput.KEY_C;
public int quests = KeyInput.KEY_V;
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
public static final String[][] ENTRIES = {
@@ -26,8 +30,12 @@ public class KeyBindings {
{"sprint", "Rennen"},
{"walk", "Gehen"},
{"interact", "Interagieren"},
{"turnLeft", "Kamera links"},
{"turnRight", "Kamera rechts"},
{"inventory", "Inventar"},
{"quicksave", "Schnellspeichern"},
{"character", "Charakter"},
{"quests", "Questlog"},
};
public int get(String fieldName) {

View File

@@ -1,51 +1,16 @@
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.post.FilterPostProcessor;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.asset.AssetManager;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Quad;
import de.blight.game.post.GaussianBlurFilter;
import de.blight.game.scene.WorldScene;
import de.blight.lang.TextResolver;
public class PauseMenu extends BaseAppState {
public class PauseMenu extends MenuScreen {
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
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_SPEICHERN = 3;
private static final int BTN_BEENDEN = 4;
private SimpleApplication app;
private Node guiNode;
private BitmapFont font;
private Node panel;
private Node bgLayer;
private Node canvasNode;
private Runnable onSave;
private Runnable onGraphics;
private Runnable onAudio;
private Runnable onControls;
private GaussianBlurFilter blurFilter;
private final float[][] btnBounds = new float[5][4];
private final Runnable onSave;
private final Runnable onGraphics;
private final Runnable onAudio;
private final Runnable onControls;
public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onAudio, Runnable onControls) {
this.onSave = onSave;
@@ -55,139 +20,39 @@ public class PauseMenu extends BaseAppState {
}
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
protected boolean withBlur() { return true; }
@Override
protected Node buildBgLayer() {
return MenuCanvas.createPauseBgLayer(assets, app.getCamera());
}
@Override
protected void onEnable() {
buildUI();
app.getInputManager().setCursorVisible(true);
app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addListener(clickListener, "_PauseClick");
addBlur();
}
protected void buildUI() {
initPanel(t("menu.pause.title"));
@Override
protected void onDisable() {
removeBlur();
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
panel = null;
app.getInputManager().deleteMapping("_PauseClick");
app.getInputManager().setCursorVisible(false);
}
private void addBlur() {
WorldScene ws = getApplication().getStateManager().getState(WorldScene.class);
if (ws == null) return;
FilterPostProcessor fpp = ws.getSharedFPP();
if (fpp == null) return;
blurFilter = new GaussianBlurFilter(6f);
fpp.addFilter(blurFilter);
}
private void removeBlur() {
if (blurFilter == null) return;
WorldScene ws = getApplication().getStateManager().getState(WorldScene.class);
if (ws != null) {
FilterPostProcessor fpp = ws.getSharedFPP();
if (fpp != null) fpp.removeFilter(blurFilter);
}
blurFilter = null;
}
@Override protected void cleanup(Application app) {}
private void buildUI() {
float sw = MenuCanvas.REF_W;
float sh = MenuCanvas.REF_H;
bgLayer = MenuCanvas.createPauseBgLayer(app.getAssetManager(), app.getCamera());
canvasNode = MenuCanvas.createFixedCanvas(app.getCamera());
guiNode.attachChild(bgLayer);
guiNode.attachChild(canvasNode);
panel = new Node("pause-panel");
float pw = 320, ph = 430;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
BitmapText title = txt(t("menu.pause.title"), 26, COL_TEXT);
centerText(title, px, py + ph - 48, pw);
panel.attachChild(title);
String[] labelKeys = {
String[] keys = {
"menu.pause.btn.graphics",
"menu.pause.btn.audio",
"menu.pause.btn.controls",
"menu.pause.btn.save",
"menu.pause.btn.quit"
};
Runnable[] acts = { onGraphics, onAudio, onControls, onSave, () -> app.stop() };
BtnStyle[] styles = {
BtnStyle.DEFAULT, BtnStyle.DEFAULT, BtnStyle.DEFAULT,
BtnStyle.SAVE, BtnStyle.QUIT
};
float bw = 260, bh = 52;
float bx = px + (pw - bw) / 2f;
float startY = py + ph - 112;
float step = 62;
float bw = 260f, bh = 52f;
float bx = CONT_X + (CONT_W - bw) / 2f;
float startY = CONT_Y + CONT_H - HDR_H - 72f;
float step = 62f;
for (int i = 0; i < 5; i++) {
float by = startY - i * step;
NinePatch btnPatch = switch (i) {
case 3 -> NinePatch.buttonSave(app.getAssetManager());
case 4 -> NinePatch.buttonQuit(app.getAssetManager());
default -> NinePatch.button(app.getAssetManager());
};
panel.attachChild(btnPatch.build(bx, by, bw, bh, 0));
BitmapText lbl = txt(t(labelKeys[i]), 18, COL_TEXT);
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;
addButton(panel, t(keys[i]), bx, startY - i * step, bw, bh, styles[i], acts[i]);
}
canvasNode.attachChild(panel);
}
private final ActionListener clickListener = (name, isPressed, tpf) -> {
if (!isPressed) return;
Vector2f c = app.getInputManager().getCursorPosition();
// Kein Scaling einfach den Zentrumversatz abziehen
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W) / 2f;
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H) / 2f;
float vx = c.x - ox;
float vy = c.y - oy;
for (int i = 0; i < 5; i++) {
if (!hits(vx, vy, 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 -> { if (onAudio != null) onAudio.run(); }
case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); }
case BTN_BEENDEN -> app.stop();
}
return;
}
};
private static String t(String id) { return TextResolver.get().resolveId(id); }
private BitmapText txt(String s, int size, ColorRGBA color) {
BitmapText t = new BitmapText(font);
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(float px, float py, float x, float y, float w, float h) {
return px >= x && px <= x + w && py >= y && py <= y + h;
}
}

View File

@@ -3,10 +3,12 @@ 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.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import de.blight.game.config.KeyBindings;
/**
* Third-Person-Kamera:
@@ -17,6 +19,7 @@ import com.jme3.scene.Spatial;
public class ThirdPersonCamera {
private static final float MOUSE_SENSITIVITY = 1.8f;
private static final float KB_TURN_SPEED = 2.5f;
private static final float BASE_DISTANCE = 5f;
private static final float MIN_DISTANCE = BASE_DISTANCE - 3f;
private static final float MAX_DISTANCE = BASE_DISTANCE + 3f;
@@ -26,6 +29,7 @@ public class ThirdPersonCamera {
private final Camera cam;
private final InputManager inputManager;
private final KeyBindings keyBindings;
private Spatial target;
@@ -34,9 +38,10 @@ public class ThirdPersonCamera {
private float distance = BASE_DISTANCE;
private boolean paused = false;
public ThirdPersonCamera(Camera cam, InputManager inputManager) {
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
this.cam = cam;
this.inputManager = inputManager;
this.keyBindings = keyBindings;
registerMappings();
}
@@ -49,19 +54,24 @@ public class ThirdPersonCamera {
// -----------------------------------------------------------------------
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));
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));
inputManager.addMapping("CamTurnLeft", new KeyTrigger(keyBindings.turnLeft));
inputManager.addMapping("CamTurnRight", new KeyTrigger(keyBindings.turnRight));
AnalogListener analogListener = (name, value, tpf) -> {
if (paused) return;
switch (name) {
// Horizontale Rotation
case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
// Horizontale Rotation — Maus
case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
// Horizontale Rotation — Tastatur
case "CamTurnLeft" -> yaw += value * KB_TURN_SPEED;
case "CamTurnRight" -> yaw -= value * KB_TURN_SPEED;
// 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);
@@ -71,7 +81,8 @@ public class ThirdPersonCamera {
}
};
inputManager.addListener(analogListener,
"MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut");
"MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut",
"CamTurnLeft", "CamTurnRight");
}
public void update(float tpf) {

View File

@@ -20,6 +20,7 @@ import com.jme3.scene.shape.*;
import com.jme3.shadow.*;
import com.jme3.terrain.geomipmap.*;
import com.jme3.texture.*;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import de.blight.common.MapData;
@@ -298,7 +299,7 @@ public class WorldScene extends BaseAppState {
log.warn("[WorldScene] PathFinder nicht ladbar Navigation deaktiviert: {}", e.getMessage());
}
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager(), keyBindings);
thirdPersonCam.setTarget(character);
MainCharacter mc = findMainCharacter();
@@ -322,6 +323,14 @@ public class WorldScene extends BaseAppState {
inventoryState.setEnabled(false);
app.getStateManager().attach(inventoryState);
app.getStateManager().attach(new de.blight.game.state.HudState(mc));
app.getStateManager().attach(new de.blight.game.state.HotbarState(mc));
app.getStateManager().attach(new de.blight.game.state.CompassHudState());
de.blight.game.state.CharacterState charState = new de.blight.game.state.CharacterState(mc);
charState.setEnabled(false);
app.getStateManager().attach(charState);
de.blight.game.state.QuestState questState = new de.blight.game.state.QuestState(mc);
questState.setEnabled(false);
app.getStateManager().attach(questState);
}
// Ertrinken-System (Wassertiefe > 1,8 m → Teleport zum Strand)
@@ -907,7 +916,7 @@ public class WorldScene extends BaseAppState {
if (path != null && !path.isEmpty()) {
try {
ByteBuffer buf = loadTextureToRGBA8(path, size, am);
images.add(new Image(Image.Format.RGBA8, size, size, buf));
images.add(new Image(Image.Format.RGBA8, size, size, buf, ColorSpace.Linear));
continue;
} catch (Exception e) { log.warn("[WorldScene-Array] Slot {} nicht ladbar: {}", i, e.getMessage()); }
}
@@ -916,7 +925,7 @@ public class WorldScene extends BaseAppState {
b = (byte)(fb.b * 255), a = (byte)(fb.a * 255);
for (int p = 0; p < size * size; p++) buf.put(r).put(g).put(b).put(a);
buf.flip();
images.add(new Image(Image.Format.RGBA8, size, size, buf));
images.add(new Image(Image.Format.RGBA8, size, size, buf, ColorSpace.Linear));
}
com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images);
texArr.setWrap(Texture.WrapMode.Repeat);
@@ -1053,7 +1062,7 @@ public class WorldScene extends BaseAppState {
splatBuf.put(splatR[i]).put(map.splatG[i]).put(map.splatB[i]).put(map.splatA[i]);
}
splatBuf.flip();
Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf));
Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf, ColorSpace.Linear));
splatTex.setWrap(Texture.WrapMode.EdgeClamp);
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
splatTex.setMagFilter(Texture.MagFilter.Bilinear);
@@ -1066,7 +1075,7 @@ public class WorldScene extends BaseAppState {
.put(map.upperSplatB[i]).put(map.upperSplatA[i]);
}
upperBuf.flip();
Texture2D upperSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, upperBuf));
Texture2D upperSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, upperBuf, ColorSpace.Linear));
upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
upperSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
@@ -1083,7 +1092,7 @@ public class WorldScene extends BaseAppState {
thirdBuf.put(map.thirdSplatA[i]);
}
thirdBuf.flip();
Texture2D thirdSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, thirdBuf));
Texture2D thirdSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, thirdBuf, ColorSpace.Linear));
thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
@@ -1122,7 +1131,7 @@ public class WorldScene extends BaseAppState {
buf.put((byte)(color.r * 255)).put((byte)(color.g * 255))
.put((byte)(color.b * 255)).put((byte)(color.a * 255));
buf.flip();
return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf));
return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf, ColorSpace.Linear));
}
}

View File

@@ -0,0 +1,471 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node;
import de.blight.common.model.MainCharacter;
import de.blight.common.model.XPHelper;
import de.blight.common.model.abilities.Abilities;
import de.blight.game.config.NinePatch;
import de.blight.game.config.OverlayState;
import de.blight.game.config.UiTheme;
import java.util.ArrayList;
import java.util.List;
/**
* Charakter-Übersicht: Werte (links) und gelernte Fähigkeiten mit Scrollbar (rechts).
*/
public class CharacterState extends OverlayState {
// ── Layout ────────────────────────────────────────────────────────────────
private static final float LEFT_W = 286f;
private static final float RIGHT_X = PX + PAD + LEFT_W + 12f;
private static final float RIGHT_W = PW - PAD - LEFT_W - PAD - 12f - PAD;
private static final float SB_W = 12f;
private static final float SB_GAP = 4f;
private static final int COLS = 3;
private static final float CARD_GAP = 6f;
private static final float CONTENT_W = RIGHT_W - SB_W - SB_GAP;
private static final float CARD_W = (CONTENT_W - (COLS - 1) * CARD_GAP) / COLS;
private static final float COMBAT_CARD_H = 140f;
private static final float CRAFT_CARD_H = 110f;
private static final float SECT_H = 26f;
private static final float BAR_W_STAT = 180f;
private static final float BAR_H_STAT = 12f;
// ── Input ─────────────────────────────────────────────────────────────────
private static final String MAP_TOGGLE = "_CharToggle";
private static final String MAP_SCROLL_UP = "_CharScrollUp";
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
// ── Scroll-Zustand ────────────────────────────────────────────────────────
private Node contentPanel;
private Node skillsNode;
private Node scrollbarNode;
private float skillScrollY = 0f;
private float skillContentH = 0f;
private float skillViewH = 0f;
private boolean needsScrollbar = false;
// ── Daten ─────────────────────────────────────────────────────────────────
private final MainCharacter mc;
private record Skill(String name, int lvl, int maxLvl, String[][] abils) {}
public CharacterState(MainCharacter mc) {
this.mc = mc;
}
// ── OverlayState-Implementierung ──────────────────────────────────────────
@Override
protected String getTitle() { return "Charakter"; }
@Override
protected void initOverlayKeys() {
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(KeyInput.KEY_C));
app.getInputManager().addListener(toggleListener, MAP_TOGGLE);
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
app.getInputManager().addListener(scrollAnalog, MAP_SCROLL_UP, MAP_SCROLL_DOWN);
}
@Override
protected void cleanupOverlayKeys() {
app.getInputManager().deleteMapping(MAP_TOGGLE);
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
}
@Override
protected void buildContent(Node panel, float scale) {
float divX = PX + PAD + LEFT_W + 6f;
addQuad(panel, divX, PY + PAD, 1f, PH - HDR_H - PAD * 2, UiTheme.COL_DIVIDER, -17f);
buildStats(panel);
buildAbilities(panel);
}
@Override
protected void cleanup(Application application) { super.cleanup(application); }
// ── Listener ──────────────────────────────────────────────────────────────
private final ActionListener toggleListener = (name, pressed, tpf) -> {
if (pressed) { setEnabled(!isEnabled()); }
};
private final AnalogListener scrollAnalog = (name, value, tpf) -> {
if (!isEnabled() || !needsScrollbar) { return; }
float delta = value * 60f;
if (MAP_SCROLL_UP.equals(name)) {
skillScrollY = Math.max(0f, skillScrollY - delta);
} else {
float maxScroll = Math.max(0f, skillContentH - skillViewH);
skillScrollY = Math.min(maxScroll, skillScrollY + delta);
}
rebuildSkills();
};
// ── Linke Spalte: Werte ───────────────────────────────────────────────────
private void buildStats(Node panel) {
float x = PX + PAD;
float y = PY + PH - HDR_H - PAD;
String name = mc.getCharacterId() != null ? mc.getCharacterId() : "Held";
String displayName = name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
BitmapText nameTxt = crispText(displayName, UiTheme.FONT_TITLE, UiTheme.COL_GOLD);
nameTxt.setLocalTranslation(x, y, -17);
panel.attachChild(nameTxt);
y -= 26;
int lvl = mc.getLevel();
int curXp = mc.getXp();
int reqXp = XPHelper.getXpRequired(lvl);
BitmapText levelTxt = crispText("Level " + (lvl + 1), UiTheme.FONT_HEADING, UiTheme.COL_WHITE);
levelTxt.setLocalTranslation(x, y, -17);
panel.attachChild(levelTxt);
y -= 16;
float xpRatio = reqXp > 0 ? Math.min(1f, (float) curXp / reqXp) : 0f;
addQuad(panel, x, y - BAR_H_STAT, BAR_W_STAT, BAR_H_STAT, UiTheme.COL_BAR_BG, -17);
if (xpRatio > 0f) {
addQuad(panel, x, y - BAR_H_STAT, BAR_W_STAT * xpRatio, BAR_H_STAT, UiTheme.COL_XP_BAR, -16);
}
BitmapText xpTxt = crispText(curXp + " / " + reqXp + " XP", UiTheme.FONT_TINY, UiTheme.COL_MUTED);
xpTxt.setLocalTranslation(x, y - BAR_H_STAT - 2, -16);
panel.attachChild(xpTxt);
y -= BAR_H_STAT + 18;
addQuad(panel, x, y, LEFT_W - 6, 1f, UiTheme.COL_DIVIDER, -17f);
y -= 14;
y = statBar(panel, "HP", mc.getCurrentHp(), mc.getMaxHp(), UiTheme.COL_HP, x, y);
y = statBar(panel, "Stamina", mc.getCurrentStamina(), mc.getMaxStamina(), UiTheme.COL_STAMINA, x, y);
y = statBar(panel, "Mana", mc.getCurrentMana(), mc.getMaxMana(), UiTheme.COL_MANA, x, y);
y -= 8;
addQuad(panel, x, y, LEFT_W - 6, 1f, UiTheme.COL_DIVIDER, -17f);
y -= 16;
int hpReg = mc.getOpenHpRegeneration();
int mnReg = mc.getOpenManaRegeneration();
int stReg = mc.getOpenStaminaRegeneration();
BitmapText regenLbl = crispText("Regeneration", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
regenLbl.setLocalTranslation(x, y, -17);
panel.attachChild(regenLbl);
y -= 18;
regenLine(panel, "HP", hpReg, x, y); y -= 16;
regenLine(panel, "Stamina", stReg, x, y); y -= 16;
regenLine(panel, "Mana", mnReg, x, y);
}
private float statBar(Node parent, String label, int cur, int max, ColorRGBA col,
float x, float y) {
float ratio = max > 0 ? Math.min(1f, (float) cur / max) : 0f;
BitmapText lblTxt = crispText(label, UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
lblTxt.setLocalTranslation(x, y, -17);
parent.attachChild(lblTxt);
BitmapText valTxt = crispText(cur + " / " + max, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
valTxt.setLocalTranslation(x + 70, y, -17);
parent.attachChild(valTxt);
y -= 16;
addQuad(parent, x, y - BAR_H_STAT, BAR_W_STAT, BAR_H_STAT, UiTheme.COL_BAR_BG, -17);
if (ratio > 0f) {
addQuad(parent, x, y - BAR_H_STAT, BAR_W_STAT * ratio, BAR_H_STAT, col, -16);
}
return y - BAR_H_STAT - 10;
}
private void regenLine(Node parent, String label, int val, float x, float y) {
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
lblTxt.setLocalTranslation(x, y, -17);
parent.attachChild(lblTxt);
BitmapText valTxt = crispText(val > 0 ? "+" + val : "", UiTheme.FONT_BODY, col);
valTxt.setLocalTranslation(x + 70, y, -17);
parent.attachChild(valTxt);
}
// ── Rechte Spalte: Fähigkeiten ────────────────────────────────────────────
private void buildAbilities(Node panel) {
Abilities ab = mc.getAbilities();
if (ab == null) { return; }
skillScrollY = 0f;
contentPanel = panel;
skillsNode = null;
scrollbarNode = null;
skillViewH = PH - HDR_H - PAD * 2;
List<Skill> combat = learnedCombat(ab);
List<Skill> craft = learnedCraft(ab);
skillContentH = calcContentH(combat, craft);
needsScrollbar = skillContentH > skillViewH;
skillsNode = new Node("skills");
buildSkillsContent(skillsNode, combat, craft);
panel.attachChild(skillsNode);
if (needsScrollbar) {
scrollbarNode = buildScrollbarNode();
panel.attachChild(scrollbarNode);
}
}
private void rebuildSkills() {
if (contentPanel == null) { return; }
if (skillsNode != null) { contentPanel.detachChild(skillsNode); skillsNode = null; }
if (scrollbarNode != null) { contentPanel.detachChild(scrollbarNode); scrollbarNode = null; }
Abilities ab = mc.getAbilities();
if (ab == null) { return; }
List<Skill> combat = learnedCombat(ab);
List<Skill> craft = learnedCraft(ab);
skillsNode = new Node("skills");
buildSkillsContent(skillsNode, combat, craft);
contentPanel.attachChild(skillsNode);
if (needsScrollbar) {
scrollbarNode = buildScrollbarNode();
contentPanel.attachChild(scrollbarNode);
}
}
private void buildSkillsContent(Node out, List<Skill> combat, List<Skill> craft) {
float visTop = PY + PH - HDR_H - PAD;
float visBot = PY + PAD;
if (combat.isEmpty() && craft.isEmpty()) {
float cy = PY + PAD + skillViewH * 0.5f;
BitmapText line1 = crispText("— Noch keine Fähigkeiten erlernt —",
UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
line1.setLocalTranslation(RIGHT_X + 8, cy + 8, -17f);
out.attachChild(line1);
BitmapText line2 = crispText("Fähigkeiten erscheinen hier, sobald du Punkte investierst.",
UiTheme.FONT_SMALL, UiTheme.COL_LOCKED);
line2.setLocalTranslation(RIGHT_X + 8, cy - 8, -17f);
out.attachChild(line2);
return;
}
// Virtueller Y-Cursor (ohne Scroll), zählt von oben nach unten
float vy = visTop;
if (!combat.isEmpty()) {
if (inView(vy - SECT_H, vy, visBot, visTop)) {
addQuad(out, RIGHT_X, vy - SECT_H - skillScrollY, CONTENT_W, SECT_H,
UiTheme.COL_SECTION, -18f);
BitmapText t = crispText("Kampf", UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
t.setLocalTranslation(RIGHT_X + 8, vy - 6 - skillScrollY, -17f);
out.attachChild(t);
}
vy -= SECT_H + 4;
int combatRows = (combat.size() + COLS - 1) / COLS;
for (int i = 0; i < combat.size(); i++) {
int col = i % COLS;
int row = i / COLS;
float cx = RIGHT_X + col * (CARD_W + CARD_GAP);
float top = vy - row * (COMBAT_CARD_H + CARD_GAP);
float bot = top - COMBAT_CARD_H;
if (inView(bot, top, visBot, visTop)) {
Skill s = combat.get(i);
buildSkillCard(out, s.name(), s.lvl(), s.maxLvl(), s.abils(),
cx, bot - skillScrollY, CARD_W, COMBAT_CARD_H);
}
}
vy -= combatRows * (COMBAT_CARD_H + CARD_GAP) + 12;
}
if (!craft.isEmpty()) {
if (inView(vy - SECT_H, vy, visBot, visTop)) {
addQuad(out, RIGHT_X, vy - SECT_H - skillScrollY, CONTENT_W, SECT_H,
UiTheme.COL_SECTION, -18f);
BitmapText t = crispText("Handwerk", UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
t.setLocalTranslation(RIGHT_X + 8, vy - 6 - skillScrollY, -17f);
out.attachChild(t);
}
vy -= SECT_H + 4;
for (int i = 0; i < craft.size(); i++) {
int col = i % COLS;
int row = i / COLS;
float cx = RIGHT_X + col * (CARD_W + CARD_GAP);
float top = vy - row * (CRAFT_CARD_H + CARD_GAP);
float bot = top - CRAFT_CARD_H;
if (inView(bot, top, visBot, visTop)) {
Skill s = craft.get(i);
buildSkillCard(out, s.name(), s.lvl(), s.maxLvl(), s.abils(),
cx, bot - skillScrollY, CARD_W, CRAFT_CARD_H);
}
}
}
}
private boolean inView(float virtualBot, float virtualTop, float visBot, float visTop) {
float sBot = virtualBot - skillScrollY;
float sTop = virtualTop - skillScrollY;
return sTop > visBot && sBot < visTop;
}
private float calcContentH(List<Skill> combat, List<Skill> craft) {
float h = 0;
if (!combat.isEmpty()) {
int rows = (combat.size() + COLS - 1) / COLS;
h += SECT_H + 4 + rows * (COMBAT_CARD_H + CARD_GAP) + 12;
}
if (!craft.isEmpty()) {
int rows = (craft.size() + COLS - 1) / COLS;
h += SECT_H + 4 + rows * (CRAFT_CARD_H + CARD_GAP);
}
return h;
}
private Node buildScrollbarNode() {
Node sb = new Node("scrollbar");
float sbX = RIGHT_X + CONTENT_W + SB_GAP;
float bot = PY + PAD;
sb.attachChild(NinePatch.scrollbarTrack(assets).build(sbX, bot, SB_W, skillViewH, -17f));
float maxScroll = skillContentH - skillViewH;
float thumbH = Math.max(20f, skillViewH * (skillViewH / skillContentH));
float ratio = maxScroll > 0 ? skillScrollY / maxScroll : 0f;
float thumbY = bot + (skillViewH - thumbH) * (1f - ratio);
sb.attachChild(NinePatch.scrollbarThumb(assets).build(sbX, thumbY, SB_W, thumbH, -16f));
return sb;
}
// ── Skill-Karte ───────────────────────────────────────────────────────────
private void buildSkillCard(Node panel, String name, int lvl, int maxLvl,
String[][] abils, float x, float y, float w, float h) {
addQuad(panel, x, y, w, h, UiTheme.COL_CARD_BRD, -18);
addQuad(panel, x+1, y+1, w-2, h-2, UiTheme.COL_CARD, -17);
float iy = y + h - 4;
iy -= 16;
String displayName = name.length() > 14 ? name.substring(0, 13) + "" : name;
BitmapText nameTxt = crispText(displayName, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
nameTxt.setLocalTranslation(x + 5, iy, -16);
panel.attachChild(nameTxt);
iy -= 4;
iy -= 10;
float pipSz = 7f;
float pipGap = 2f;
float totalPips = maxLvl * (pipSz + pipGap) - pipGap;
float pipX = x + (w - totalPips) / 2f;
for (int p = 0; p < maxLvl; p++) {
ColorRGBA pipCol = p < lvl ? UiTheme.COL_PIP_ON : UiTheme.COL_PIP_OFF;
addQuad(panel, pipX + p * (pipSz + pipGap), iy - pipSz, pipSz, pipSz, pipCol, -16);
}
BitmapText lvlTxt = crispText("Lv. " + lvl, UiTheme.FONT_TINY, UiTheme.COL_MUTED);
lvlTxt.setLocalTranslation(x + 5, iy, -16);
panel.attachChild(lvlTxt);
iy -= pipSz + 6;
addQuad(panel, x + 4, iy, w - 8, 1f, UiTheme.COL_DIVIDER, -16f);
iy -= 12;
for (String[] abil : abils) {
if (iy < y + 4) { break; }
String abilName = abil[0];
int minLvl = Integer.parseInt(abil[1]);
boolean unlocked = lvl >= minLvl;
ColorRGBA col = unlocked ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
String prefix = unlocked ? "" : "";
BitmapText abilTxt = crispText(prefix + abilName, UiTheme.FONT_SMALL, col);
abilTxt.setLocalTranslation(x + 5, iy, -16);
panel.attachChild(abilTxt);
iy -= 14;
}
}
// ── Fähigkeitenlisten (nur gelernte) ──────────────────────────────────────
private List<Skill> learnedCombat(Abilities ab) {
List<Skill> list = new ArrayList<>();
if (ab.getLvlSwordsmanship() > 0)
list.add(new Skill("Schwertkampf", ab.getLvlSwordsmanship(), 10, swordAbilNames()));
if (ab.getLvlStaffCombat() > 0)
list.add(new Skill("Stabkampf", ab.getLvlStaffCombat(), 10, staffAbilNames()));
if (ab.getLvlMagic() > 0)
list.add(new Skill("Magie", ab.getLvlMagic(), 10, magicAbilNames()));
if (ab.getLvlHeavyWeapons() > 0)
list.add(new Skill("Schwere Waffen", ab.getLvlHeavyWeapons(), 10, heavyAbilNames()));
if (ab.getLvlArchery() > 0)
list.add(new Skill("Bogenschuss", ab.getLvlArchery(), 10, archerAbilNames()));
if (ab.getLvlCrossbow() > 0)
list.add(new Skill("Armbrust", ab.getLvlCrossbow(), 10, crossbowAbilNames()));
return list;
}
private List<Skill> learnedCraft(Abilities ab) {
List<Skill> list = new ArrayList<>();
if (ab.getLvlThievery() > 0)
list.add(new Skill("Diebstahl",
ab.getLvlThievery(), 3, craftDescs("Schlösser knacken", "Taschendiebstahl", "Schleichen")));
if (ab.getLvlAlchemy() > 0)
list.add(new Skill("Alchemie",
ab.getLvlAlchemy(), 3, craftDescs("Kräuter-Tränke", "Temp. Verstärkung", "Dauer-Tränke")));
if (ab.getLvlEngineering() > 0)
list.add(new Skill("Ingenieurskunst",
ab.getLvlEngineering(), 3, craftDescs("Bandagen & Spritzen", "Wurfbomben", "Artillerie")));
if (ab.getLvlSmithery() > 0)
list.add(new Skill("Schmiedekunst",
ab.getLvlSmithery(), 3, craftDescs("Einfache Waffen", "Mittlere Ausrüst.", "Meisterwerke")));
if (ab.getLvlEnchanting() > 0)
list.add(new Skill("Verzauberung",
ab.getLvlEnchanting(), 3, craftDescs("Schwache Gegenstände", "Mittelst. Gegenstände", "Mächtige Gegenstände")));
return list;
}
// ── Fähigkeiten-Daten ─────────────────────────────────────────────────────
private static String[][] swordAbilNames() {
return new String[][]{{"Basisangriff","1"},{"Block","3"},{"Schwerer Schlag","5"},
{"Gezielter Stich","7"},{"Klingentanz","9"},{"Todesstoß","10"}};
}
private static String[][] staffAbilNames() {
return new String[][]{{"Basisangriff","1"},{"Block","3"},{"Schwerer Schlag","5"},
{"Standfestigkeit","7"},{"Finte","9"},{"Entwaffnen","10"}};
}
private static String[][] magicAbilNames() {
return new String[][]{{"Feuerball","1"},{"Licht","1"},{"Schild","3"},
{"Verwurzelung","5"},{"Schockwelle","7"},{"Lebensraub","9"},{"Kettenblitz","10"}};
}
private static String[][] heavyAbilNames() {
return new String[][]{{"Basisangriff","1"},{"Block","3"},{"Schwerer Schlag","5"},
{"Wirbelangriff","7"},{"Doppelangriff","9"},{"Exekution","10"}};
}
private static String[][] archerAbilNames() {
return new String[][]{{"Schuss","1"},{"Präzisionsschuss","3"},{"Schnelles Laden","5"},
{"Durchschlag","7"},{"Doppelschuss","9"},{"Adlerauge","10"}};
}
private static String[][] crossbowAbilNames() {
return new String[][]{{"Schuss","1"},{"Zielsicherheit","3"},{"Schnelles Laden","5"},
{"Durchschlag","7"},{"Doppelbolzen","9"},{"Scharfschütze","10"}};
}
private static String[][] craftDescs(String d1, String d2, String d3) {
return new String[][]{{d1,"1"},{d2,"2"},{d3,"3"}};
}
}

View File

@@ -0,0 +1,157 @@
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.font.BitmapFont;
import com.jme3.font.BitmapText;
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.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
/**
* Kompass unten links. Zeigt N/O/S/W-Labels, die um das Kompasszentrum
* rotieren wenn sich die Kamera dreht. Eine feste Markierung oben
* zeigt immer die aktuelle Blickrichtung an.
*/
public class CompassHudState extends BaseAppState {
// ── Layout ────────────────────────────────────────────────────────────────
private static final float SIZE = 76f; // Kompass-Durchmesser (Pixel)
private static final float RADIUS = 26f; // Label-Radius vom Mittelpunkt
private static final float MARGIN = 16f; // Abstand zum Bildschirmrand
private static final float LABEL_Z = 4f;
private static final float DOT_SZ = 6f; // Vorwärts-Marker
private static final ColorRGBA COL_BG = new ColorRGBA(0.06f, 0.06f, 0.10f, 0.88f);
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.30f, 0.30f, 0.44f, 1.00f);
private static final ColorRGBA COL_N = new ColorRGBA(0.90f, 0.20f, 0.20f, 1.00f);
private static final ColorRGBA COL_CARD = new ColorRGBA(0.75f, 0.75f, 0.78f, 1.00f);
private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.90f, 0.20f, 1.00f);
// ── Himmelsrichtungen: Name, Winkel-Offset von Nord (Rad) ─────────────────
private static final String[] LABELS = {"N", "O", "S", "W"};
private static final float[] OFFSETS = {0f, FastMath.HALF_PI, FastMath.PI, -FastMath.HALF_PI};
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private BitmapFont font;
private Node compassNode;
private float cx, cy; // Mittelpunkt (Bildschirmkoordinaten)
private final BitmapText[] dirLabels = new BitmapText[4];
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
cam = app.getCamera();
assets = app.getAssetManager();
font = assets.loadFont("Interface/Fonts/Default.fnt");
}
@Override
protected void onEnable() {
buildCompass();
}
@Override
protected void onDisable() {
if (compassNode != null) {
app.getGuiNode().detachChild(compassNode);
compassNode = null;
}
}
@Override
protected void cleanup(Application application) {}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
if (compassNode == null) { return; }
Vector3f dir = cam.getDirection();
// Yaw vom Weltursprung: 0 = Blick nach +Z (Nord), π/2 = Osten (+X)
float yaw = FastMath.atan2(dir.x, dir.z);
for (int i = 0; i < 4; i++) {
// Winkel des Labels im Kompass-Raum (von Oben CW = von +Y Achse)
float a = -yaw + OFFSETS[i];
float lx = cx + RADIUS * FastMath.sin(a);
float ly = cy + RADIUS * FastMath.cos(a);
BitmapText lbl = dirLabels[i];
lbl.setLocalTranslation(lx - lbl.getLineWidth() * 0.5f, ly + lbl.getLineHeight() * 0.4f, LABEL_Z);
}
}
// ── Aufbau ───────────────────────────────────────────────────────────────
private void buildCompass() {
float sw = cam.getWidth();
float sh = cam.getHeight();
// Kompass unten links
float ox = MARGIN;
// Direkt über der Hotbar: Hotbar.MARGIN_BOT + SLOT_SIZE + Gap
float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f;
cx = ox + SIZE / 2f;
cy = oy + SIZE / 2f;
compassNode = new Node("compass");
// Rahmen
compassNode.attachChild(makeQuad(ox, oy, SIZE, SIZE, COL_BORDER, 1f));
// Hintergrund
compassNode.attachChild(makeQuad(ox + 1, oy + 1, SIZE - 2, SIZE - 2, COL_BG, 2f));
// Vorwärts-Marker: kleine goldene Raute oben in der Mitte
compassNode.attachChild(makeQuad(cx - DOT_SZ * 0.5f, cy + RADIUS - DOT_SZ * 0.5f,
DOT_SZ, DOT_SZ, COL_MARKER, 3f));
// Himmelsrichtungs-Labels (Positionen werden in update() gesetzt)
for (int i = 0; i < 4; i++) {
ColorRGBA col = i == 0 ? COL_N : COL_CARD;
BitmapText lbl = txt(LABELS[i], 13, col);
lbl.setLocalTranslation(cx, cy, LABEL_Z);
dirLabels[i] = lbl;
compassNode.attachChild(lbl);
}
app.getGuiNode().attachChild(compassNode);
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private Geometry makeQuad(float x, float y, float w, float h, ColorRGBA col, float z) {
Geometry g = new Geometry("q", new Quad(w, h));
Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
m.setColor("Color", col.clone());
if (col.a < 1f) {
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
} else {
g.setQueueBucket(RenderQueue.Bucket.Gui);
}
g.setMaterial(m);
g.setLocalTranslation(x, y, z);
return g;
}
private BitmapText txt(String s, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setColor(col);
t.setText(s);
t.setQueueBucket(RenderQueue.Bucket.Gui);
return t;
}
}

View File

@@ -47,7 +47,7 @@ public class GrassState extends BaseAppState
private Node grassNode;
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
private final List<GrassTuft>[] chunkTufts = new List[CHUNK_COUNT];
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
private final Map<Integer, Material> slotMaterials = new LinkedHashMap<>();

View File

@@ -63,7 +63,7 @@ public class GrassVertexRenderState extends BaseAppState
// ── Samen-Texturen ────────────────────────────────────────────────────────
private static final String SEED_TEX_BASE = "Textures/internal/gras/seeds/seeds";
private static final String SEED_TEX_EXT = ".png";
private static final float SEED_SIZE_FACTOR = 0.45f;
private static final float SEED_SIZE_FACTOR = 0.225f;
private static final float SEED_Y_FACTOR = 0.78f;
// ── Zustand ───────────────────────────────────────────────────────────────
@@ -75,7 +75,7 @@ public class GrassVertexRenderState extends BaseAppState
private Material seedStalkMaterial;
private int nextChunk = 0;
@SuppressWarnings("unchecked")
@SuppressWarnings({"unchecked", "rawtypes"})
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
@@ -357,7 +357,8 @@ public class GrassVertexRenderState extends BaseAppState
float x = b.x();
float yBot = b.y() + b.height() * SEED_Y_FACTOR;
float z = b.z();
float size = b.height() * SEED_SIZE_FACTOR;
float seedRand = hash(b.x() * 17.3f, b.z() * 31.9f);
float size = b.height() * SEED_SIZE_FACTOR * (0.75f + 0.5f * seedRand);
float hw = size * 0.5f;
// Quad 1 entlang Welt-X

View File

@@ -0,0 +1,261 @@
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.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
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.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import de.blight.common.model.Item;
import de.blight.common.model.MainCharacter;
/**
* Immer sichtbare Schnellzugriffsleiste (Hotbar) mit 10 Slots (Tasten 19, 0).
* Items können per Drag & Drop aus dem Inventar in Slots gezogen werden.
* Aktiver Slot wird durch eine goldene Umrandung hervorgehoben.
*/
public class HotbarState extends BaseAppState {
// ── Layout ────────────────────────────────────────────────────────────────
static final int SLOT_COUNT = 10;
static final float SLOT_SIZE = 52f;
static final float SLOT_BORDER = 2f;
static final float SLOT_GAP = 4f;
static final float SLOT_STRIDE = SLOT_SIZE + SLOT_GAP;
static final float THUMB_SZ = SLOT_SIZE - SLOT_BORDER * 2 - 4f;
static final float HOTBAR_W = SLOT_COUNT * SLOT_STRIDE - SLOT_GAP;
static final float MARGIN_BOT = 16f;
private static final ColorRGBA COL_BG = new ColorRGBA(0.08f, 0.08f, 0.12f, 0.90f);
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.30f, 0.30f, 0.44f, 1.00f);
private static final ColorRGBA COL_ACTIVE = new ColorRGBA(0.82f, 0.76f, 0.18f, 1.00f);
private static final ColorRGBA COL_KEY = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f);
private static final ColorRGBA COL_COUNT = ColorRGBA.White;
private static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.18f, 0.18f, 0.26f, 1.00f);
private static final String[] SLOT_LABEL = {"1","2","3","4","5","6","7","8","9","0"};
private static final int[] KEY_CODES = {
KeyInput.KEY_1, KeyInput.KEY_2, KeyInput.KEY_3, KeyInput.KEY_4, KeyInput.KEY_5,
KeyInput.KEY_6, KeyInput.KEY_7, KeyInput.KEY_8, KeyInput.KEY_9, KeyInput.KEY_0
};
// ── Zustand ───────────────────────────────────────────────────────────────
private final MainCharacter mc;
private SimpleApplication app;
private AssetManager assets;
private BitmapFont font;
private Node hotbarNode;
private float originX, originY;
final Item[] slots = new Item[SLOT_COUNT];
private int activeSlot = 0;
private final Geometry[] borderGeoms = new Geometry[SLOT_COUNT];
private final Node[] thumbNodes = new Node[SLOT_COUNT];
private final BitmapText[] countLabels = new BitmapText[SLOT_COUNT];
// ── Konstruktor ───────────────────────────────────────────────────────────
public HotbarState(MainCharacter mc) {
this.mc = mc;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
assets = app.getAssetManager();
font = assets.loadFont("Interface/Fonts/Default.fnt");
}
@Override
protected void onEnable() {
buildHotbar();
registerKeys();
}
@Override
protected void onDisable() {
if (hotbarNode != null) {
app.getGuiNode().detachChild(hotbarNode);
hotbarNode = null;
}
unregisterKeys();
}
@Override
protected void cleanup(Application application) {}
// ── Aufbau ───────────────────────────────────────────────────────────────
private void buildHotbar() {
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
originX = (sw - HOTBAR_W) / 2f;
originY = MARGIN_BOT;
hotbarNode = new Node("hotbar");
for (int i = 0; i < SLOT_COUNT; i++) {
float sx = originX + i * SLOT_STRIDE;
float sy = originY;
buildSlot(i, sx, sy);
}
app.getGuiNode().attachChild(hotbarNode);
}
private void buildSlot(int idx, float x, float y) {
// Rahmen
Geometry border = quad(x, y, SLOT_SIZE, SLOT_SIZE,
idx == activeSlot ? COL_ACTIVE : COL_BORDER, 1f);
borderGeoms[idx] = border;
hotbarNode.attachChild(border);
// Hintergrund
hotbarNode.attachChild(quad(x + SLOT_BORDER, y + SLOT_BORDER,
SLOT_SIZE - SLOT_BORDER * 2, SLOT_SIZE - SLOT_BORDER * 2, COL_BG, 2f));
// Thumbnail-Bereich (Node für einfaches Ersetzen)
Node tn = new Node("thumb_" + idx);
tn.setLocalTranslation(x + SLOT_BORDER + 2f, y + SLOT_BORDER + 2f + 8f, 3f);
thumbNodes[idx] = tn;
hotbarNode.attachChild(tn);
refreshThumb(idx);
// Taste-Label (oben links)
BitmapText keyLbl = txt(SLOT_LABEL[idx], 11, COL_KEY);
keyLbl.setLocalTranslation(x + 3f, y + SLOT_SIZE - 2f, 4f);
hotbarNode.attachChild(keyLbl);
// Anzahl-Label (unten rechts)
BitmapText cntLbl = txt("", 11, COL_COUNT);
cntLbl.setLocalTranslation(x + SLOT_SIZE - 20f, y + 14f, 4f);
countLabels[idx] = cntLbl;
hotbarNode.attachChild(cntLbl);
}
// ── Thumb aktualisieren ───────────────────────────────────────────────────
private void refreshThumb(int idx) {
Node tn = thumbNodes[idx];
tn.detachAllChildren();
Item item = slots[idx];
if (item == null) {
tn.attachChild(quad(0, 0, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, 0f));
} else {
Texture tex = loadThumb(item);
if (tex != null) {
Geometry g = new Geometry("t", new Quad(THUMB_SZ, THUMB_SZ));
Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
m.setTexture("ColorMap", tex);
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setMaterial(m);
g.setQueueBucket(RenderQueue.Bucket.Gui);
tn.attachChild(g);
} else {
tn.attachChild(quad(0, 0, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, 0f));
}
// Anzahl aus Inventar
int count = mc.getInventar() != null
? mc.getInventar().getItems().getOrDefault(item, 0) : 0;
countLabels[idx].setText(count > 1 ? String.valueOf(count) : "");
}
}
private void refreshBorder(int idx) {
borderGeoms[idx].getMaterial().setColor("Color",
idx == activeSlot ? COL_ACTIVE.clone() : COL_BORDER.clone());
}
// ── Drop-API für InventoryState ───────────────────────────────────────────
/**
* Nimmt ein per Drag & Drop abgelegtes Item an, sofern {@code screenPos}
* auf einen Slot zeigt. Gibt true zurück wenn der Drop angenommen wurde.
*/
public boolean tryDrop(Vector2f screenPos, Item item) {
for (int i = 0; i < SLOT_COUNT; i++) {
float sx = originX + i * SLOT_STRIDE;
if (screenPos.x >= sx && screenPos.x <= sx + SLOT_SIZE
&& screenPos.y >= originY && screenPos.y <= originY + SLOT_SIZE) {
slots[i] = item;
refreshThumb(i);
return true;
}
}
return false;
}
// ── Tasten ────────────────────────────────────────────────────────────────
private void registerKeys() {
for (int i = 0; i < SLOT_COUNT; i++) {
String m = "_Hotbar" + i;
app.getInputManager().addMapping(m, new KeyTrigger(KEY_CODES[i]));
final int slot = i;
app.getInputManager().addListener(
(ActionListener) (name, pressed, tpf) -> { if (pressed) selectSlot(slot); }, m);
}
}
private void unregisterKeys() {
for (int i = 0; i < SLOT_COUNT; i++) {
try { app.getInputManager().deleteMapping("_Hotbar" + i); }
catch (Exception ignored) {}
}
}
private void selectSlot(int idx) {
int prev = activeSlot;
activeSlot = idx;
refreshBorder(prev);
refreshBorder(idx);
// Item benutzen wenn Slot belegt und konsumierbar
Item item = slots[idx];
if (item != null && item.isConsumable() && mc != null) {
item.use(mc);
}
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private Texture loadThumb(Item item) {
if (item.getModelRef() == null) return null;
String tp = item.getModelRef().getThumbnailAssetPath();
if (tp == null) return null;
try { return assets.loadTexture(tp); } catch (Exception e) { return null; }
}
private Geometry quad(float x, float y, float w, float h, ColorRGBA col, float z) {
Geometry g = new Geometry("q", new Quad(w, h));
Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
m.setColor("Color", col.clone());
if (col.a < 1f) {
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
} else {
g.setQueueBucket(RenderQueue.Bucket.Gui);
}
g.setMaterial(m);
g.setLocalTranslation(x, y, z);
return g;
}
private BitmapText txt(String s, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setColor(col);
t.setText(s);
t.setQueueBucket(RenderQueue.Bucket.Gui);
return t;
}
}

View File

@@ -1,10 +1,6 @@
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.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
@@ -20,14 +16,13 @@ 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.post.FilterPostProcessor;
import com.jme3.texture.Texture;
import de.blight.common.model.*;
import de.blight.game.config.KeyBindings;
import de.blight.game.config.MenuCanvas;
import de.blight.game.config.NinePatch;
import de.blight.game.post.GaussianBlurFilter;
import de.blight.game.scene.WorldScene;
import de.blight.game.config.OverlayState;
import de.blight.game.config.UiTheme;
import java.util.*;
import java.util.stream.Collectors;
@@ -39,24 +34,14 @@ import java.util.stream.Collectors;
* SubKategorie und dann nach Preis sortiert angezeigt.
* Thumbnails werden aus {@code ObjectReference.getThumbnailAssetPath()} geladen.
*/
public class InventoryState extends BaseAppState {
public class InventoryState extends OverlayState {
// ── Farben ────────────────────────────────────────────────────────────────
// ── Inventar-spezifische Farben ───────────────────────────────────────────
private static final ColorRGBA COL_OVERLAY = new ColorRGBA(0f, 0f, 0f, 0.72f);
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.09f, 0.09f, 0.14f, 0.97f);
private static final ColorRGBA COL_HDR = new ColorRGBA(0.05f, 0.05f, 0.09f, 1.00f);
private static final ColorRGBA COL_TAB_ON = new ColorRGBA(0.22f, 0.22f, 0.42f, 1.00f);
private static final ColorRGBA COL_TAB_OFF = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f);
private static final ColorRGBA COL_CELL = new ColorRGBA(0.14f, 0.14f, 0.22f, 1.00f);
private static final ColorRGBA COL_CELL_FRAME = new ColorRGBA(0.22f, 0.22f, 0.35f, 1.00f);
private static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.20f, 0.20f, 0.28f, 1.00f);
private static final ColorRGBA COL_BADGE = new ColorRGBA(0.04f, 0.04f, 0.07f, 0.92f);
private static final ColorRGBA COL_SUBHDR = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.80f);
private static final ColorRGBA COL_WHITE = ColorRGBA.White;
private static final ColorRGBA COL_MUTED = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f);
private static final ColorRGBA COL_GOLD = new ColorRGBA(1.00f, 0.85f, 0.20f, 1.00f);
private static final ColorRGBA COL_SUBCAT_TXT = new ColorRGBA(0.55f, 0.78f, 1.00f, 1.00f);
private static final ColorRGBA COL_THUMB_BG = UiTheme.COL_THUMB_BG;
private static final ColorRGBA COL_BADGE = UiTheme.COL_BADGE;
private static final ColorRGBA COL_SUBHDR = UiTheme.COL_SUBHDR;
private static final ColorRGBA COL_SUBCAT_TXT = UiTheme.COL_SUBCAT_TXT;
// ── Layout ────────────────────────────────────────────────────────────────
@@ -65,9 +50,7 @@ public class InventoryState extends BaseAppState {
private static final int CELL_H = 195; // 128 thumb + 67 text
private static final int CELL_GAP = 8;
private static final int THUMB_SZ = 128;
private static final int HDR_H = 42;
private static final int TAB_H = 32;
private static final int PAD = 16; // panel inner padding
private static final int SUBHDR_H = 26;
private static final int SUBHDR_GAP = 4;
@@ -80,15 +63,8 @@ public class InventoryState extends BaseAppState {
// ── JME-Zustand ───────────────────────────────────────────────────────────
private SimpleApplication app;
private AssetManager assetManager;
private BitmapFont font;
private Node guiNode;
private Node panel;
private Node gridNode;
private Node bgLayer;
private Node canvasNode;
private GaussianBlurFilter blurFilter;
// ── Daten ─────────────────────────────────────────────────────────────────
@@ -98,14 +74,15 @@ public class InventoryState extends BaseAppState {
// ── UI-Zustand ────────────────────────────────────────────────────────────
private ItemCategory activeTab = null;
private float scrollY = 0f; // Pixel-Scroll-Offset (nach unten = positiv)
private float scrollY = 0f;
private float maxScrollY = 0f;
private float uiScale = 1f; // gespeichert aus buildContent
// Für Tab-Klick-Erkennung: parallele Arrays
private ItemCategory[] tabOrder;
private float[][] tabBounds; // [tabIdx] = {x, y, w, h}
private float[][] tabBounds;
// Content-Bereich in Screen-Koordinaten (JME3 Y-up)
// Content-Bereich in virtuellen Koordinaten
private float contentLeft;
private float contentBottom;
private float contentTop;
@@ -120,15 +97,15 @@ public class InventoryState extends BaseAppState {
this.keyBindings = keyBindings;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
// ── OverlayState-Implementierung ──────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assetManager = app.getAssetManager();
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
protected String getTitle() {
return "Inventar";
}
@Override
protected void initOverlayKeys() {
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(keyBindings.inventory));
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
app.getInputManager().addMapping(MAP_SCROLL_DN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
@@ -137,63 +114,65 @@ public class InventoryState extends BaseAppState {
}
@Override
protected void onEnable() {
scrollY = 0f;
buildPanel();
app.getInputManager().setCursorVisible(true);
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
app.getInputManager().addListener(clickListener, MAP_CLICK);
WorldScene ws = app.getStateManager().getState(WorldScene.class);
if (ws != null) {
ws.setPaused(true);
FilterPostProcessor fpp = ws.getSharedFPP();
if (fpp != null) {
blurFilter = new GaussianBlurFilter(6f);
fpp.addFilter(blurFilter);
}
}
}
@Override
protected void onDisable() {
destroyPanel();
app.getInputManager().removeListener(scrollListener);
app.getInputManager().removeListener(clickListener);
app.getInputManager().setCursorVisible(false);
WorldScene ws = app.getStateManager().getState(WorldScene.class);
if (ws != null) {
ws.setPaused(false);
if (blurFilter != null) {
FilterPostProcessor fpp = ws.getSharedFPP();
if (fpp != null) fpp.removeFilter(blurFilter);
blurFilter = null;
}
}
}
@Override
protected void cleanup(Application app) {
protected void cleanupOverlayKeys() {
app.getInputManager().deleteMapping(MAP_TOGGLE);
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
app.getInputManager().deleteMapping(MAP_SCROLL_DN);
app.getInputManager().deleteMapping(MAP_CLICK);
}
@Override
protected void buildContent(Node parentPanel, float scale) {
this.uiScale = scale;
this.panel = parentPanel;
// Content-Bereich (für Scroll-Berechnungen), innerhalb des OverlayState-Panels
contentLeft = PX + PAD;
contentBottom = PY + PAD;
contentTop = PY + PH - HDR_H - TAB_H - 8;
buildTabs(parentPanel);
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void onEnable() {
scrollY = 0f;
super.onEnable();
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
app.getInputManager().addListener(clickListener, MAP_CLICK);
}
@Override
protected void onDisable() {
app.getInputManager().removeListener(scrollListener);
app.getInputManager().removeListener(clickListener);
panel = null;
gridNode = null;
super.onDisable();
}
@Override
protected void cleanup(Application app) {
super.cleanup(app);
}
// ── Listener ──────────────────────────────────────────────────────────────
private final ActionListener toggleListener = (name, pressed, tpf) -> {
if (pressed && MAP_TOGGLE.equals(name)) setEnabled(!isEnabled());
if (pressed && MAP_TOGGLE.equals(name)) { setEnabled(!isEnabled()); }
};
private final AnalogListener scrollListener = (name, value, tpf) -> {
float step = 60f * value;
if (MAP_SCROLL_UP.equals(name)) scrollY = Math.max(0f, scrollY - step);
else scrollY = Math.min(maxScrollY, scrollY + step);
if (MAP_SCROLL_UP.equals(name)) { scrollY = Math.max(0f, scrollY - step); }
else { scrollY = Math.min(maxScrollY, scrollY + step); }
rebuildGrid();
};
private final ActionListener clickListener = (name, pressed, tpf) -> {
if (!pressed || panel == null || tabBounds == null) return;
if (!pressed || panel == null || tabBounds == null) { return; }
float[] v = toVirtual(app.getInputManager().getCursorPosition());
for (int i = 0; i < tabBounds.length; i++) {
float[] b = tabBounds[i];
@@ -204,60 +183,27 @@ public class InventoryState extends BaseAppState {
}
};
// ── Haupt-Panel aufbauen ──────────────────────────────────────────────────
// ── Tabs aufbauen ─────────────────────────────────────────────────────────
private void buildPanel() {
float sw = MenuCanvas.REF_W;
float sh = MenuCanvas.REF_H;
bgLayer = MenuCanvas.createBgLayer(assetManager, app.getCamera());
canvasNode = MenuCanvas.createCanvas(app.getCamera());
guiNode.attachChild(bgLayer);
guiNode.attachChild(canvasNode);
// Panelgröße: 5 Spalten + Ränder
float pw = COLS * (CELL_W + CELL_GAP) + CELL_GAP + 2 * PAD;
float ph = HDR_H + TAB_H + 4 + 450 + PAD; // header + tabs + gap + content + bottom
float px = (sw - pw) / 2f;
float py = (sh - ph) / 2f;
panel = new Node("inv-panel");
panel.attachChild(NinePatch.panel(assetManager).build(px, py, pw, ph, -19));
quad(panel, px, py + ph - HDR_H, pw, HDR_H, COL_HDR, -18); // Header-Balken
// Titel
BitmapText title = txt("Inventar", 22, COL_WHITE);
title.setLocalTranslation(px + PAD, py + ph - 12, -17);
panel.attachChild(title);
// Content-Bereich (für Scroll-Berechnungen)
contentLeft = px + PAD;
contentBottom = py + PAD;
contentTop = py + ph - HDR_H - TAB_H - 8;
// Tabs aufbauen
buildTabs(px, py, pw, ph);
canvasNode.attachChild(panel);
}
private void buildTabs(float px, float py, float pw, float ph) {
private void buildTabs(Node panel) {
tabOrder = ItemCategory.values();
tabBounds = new float[tabOrder.length][4];
if (activeTab == null) activeTab = tabOrder[0];
if (activeTab == null) { activeTab = tabOrder[0]; }
float tabY = py + ph - HDR_H - TAB_H;
float tabW = (pw - 8) / tabOrder.length;
float tabX0 = px + 4;
float tabY = PY + PH - HDR_H - TAB_H;
float tabW = (PW - 8) / tabOrder.length;
float tabX0 = PX + 4;
for (int i = 0; i < tabOrder.length; i++) {
boolean active = tabOrder[i] == activeTab;
float tx = tabX0 + i * tabW;
NinePatch tabPatch = active ? NinePatch.tabActive(assetManager) : NinePatch.tabInactive(assetManager);
panel.attachChild(tabPatch.build(tx, tabY, tabW - 4, TAB_H, -18));
BitmapText lbl = txt(catLabel(tabOrder[i]), 13, active ? COL_WHITE : COL_MUTED);
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
NinePatch tabPatch = active ? NinePatch.tabActive(assets) : NinePatch.tabInactive(assets);
panel.attachChild(tabPatch.build(tx, tabY, tabW - 4, TAB_H, -18f));
BitmapText lbl = crispText(catLabel(tabOrder[i]), UiTheme.FONT_SMALL,
active ? UiTheme.COL_WHITE : UiTheme.COL_MUTED);
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f,
tabY + TAB_H - 8, -17);
panel.attachChild(lbl);
tabBounds[i] = new float[]{ tx, tabY, tabW - 4, TAB_H };
}
@@ -269,23 +215,22 @@ public class InventoryState extends BaseAppState {
}
private void switchTab(ItemCategory cat) {
if (cat == activeTab) return;
if (cat == activeTab) { return; }
activeTab = cat;
scrollY = 0f;
activeItems = sortedItems(cat);
destroyPanel();
buildPanel();
setEnabled(false);
setEnabled(true);
}
// ── Item-Grid ─────────────────────────────────────────────────────────────
/** Erstellt den Grid-Node und hängt ihn ans Panel. */
private void buildGrid() {
gridNode = new Node("inv-grid");
panel.attachChild(gridNode);
if (activeItems.isEmpty()) {
BitmapText empty = txt("Keine Items vorhanden", 15, COL_MUTED);
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
empty.setLocalTranslation(ex, ey, -17);
@@ -293,38 +238,33 @@ public class InventoryState extends BaseAppState {
return;
}
// Items nach SubKategorie gruppiert (Reihenfolge aus sortedItems beibehalten)
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
for (Map.Entry<Item, Integer> e : activeItems) {
groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e);
}
// Virtual Y: beginnt am Top des Content-Bereichs, läuft nach unten (Y nimmt ab)
float virtY = contentTop - scrollY;
for (Map.Entry<ItemSubCategory, List<Map.Entry<Item, Integer>>> group : groups.entrySet()) {
// SubKategorie-Header
float subHdrY = virtY - SUBHDR_H;
if (subHdrY + SUBHDR_H >= contentBottom && subHdrY <= contentTop) {
String subLabel = group.getKey() != null ? subCatLabel(group.getKey()) : "Sonstiges";
float subBgW = COLS * (CELL_W + CELL_GAP) - CELL_GAP;
quad(gridNode, contentLeft, subHdrY, subBgW, SUBHDR_H, COL_SUBHDR, -18);
BitmapText sLbl = txt(" " + subLabel, 12, COL_SUBCAT_TXT);
addQuad(gridNode, contentLeft, subHdrY, subBgW, SUBHDR_H, COL_SUBHDR, -18f);
BitmapText sLbl = crispText(" " + subLabel, UiTheme.FONT_BODY, COL_SUBCAT_TXT);
sLbl.setLocalTranslation(contentLeft + 6, subHdrY + SUBHDR_H - 7, -17);
gridNode.attachChild(sLbl);
}
virtY -= SUBHDR_H + SUBHDR_GAP;
// Item-Zellen zeilenweise
List<Map.Entry<Item, Integer>> groupItems = group.getValue();
int col = 0;
for (Map.Entry<Item, Integer> entry : groupItems) {
if (col == 0) virtY -= CELL_H;
if (col == 0) { virtY -= CELL_H; }
float cellX = contentLeft + col * (CELL_W + CELL_GAP);
float cellY = virtY;
// Nur rendern wenn vollständig im sichtbaren Content-Bereich
if (cellY >= contentBottom && cellY + CELL_H <= contentTop) {
buildCell(gridNode, entry.getKey(), entry.getValue(), cellX, cellY);
}
@@ -335,20 +275,17 @@ public class InventoryState extends BaseAppState {
virtY -= CELL_GAP;
}
}
// Wenn letzte Zeile nicht voll war, Y-Schritt nachholen
if (col != 0) virtY -= CELL_GAP;
virtY -= CELL_GAP; // Abstand zwischen Gruppen
if (col != 0) { virtY -= CELL_GAP; }
virtY -= CELL_GAP;
}
}
/** Entfernt den Grid-Node und erstellt ihn neu (bei Scroll). */
private void rebuildGrid() {
if (panel == null) return;
if (gridNode != null) panel.detachChild(gridNode);
if (panel == null) { return; }
if (gridNode != null) { panel.detachChild(gridNode); }
buildGrid();
}
/** Berechnet maximalen Scroll-Offset in Pixel. */
private float computeMaxScroll() {
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
for (Map.Entry<Item, Integer> e : activeItems) {
@@ -367,67 +304,61 @@ public class InventoryState extends BaseAppState {
// ── Einzel-Zelle ──────────────────────────────────────────────────────────
private void buildCell(Node parent, Item item, int count, float x, float y) {
parent.attachChild(NinePatch.itemCell(assetManager).build(x, y, CELL_W, CELL_H, -18));
parent.attachChild(NinePatch.itemCell(assets).build(x, y, CELL_W, CELL_H, -18f));
// Thumbnail
float thumbX = x + (CELL_W - THUMB_SZ) / 2f;
float thumbY = y + CELL_H - THUMB_SZ - 6;
Texture thumb = null;
if (item.getModelRef() != null) {
String tp = item.getModelRef().getThumbnailAssetPath();
if (tp != null) thumb = loadThumb(tp);
if (tp != null) { thumb = loadThumb(tp); }
}
if (thumb != null) {
Geometry tg = new Geometry("thumb", new Quad(THUMB_SZ, THUMB_SZ));
Material tm = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
Material tm = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
tm.setTexture("ColorMap", thumb);
tm.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
tg.setQueueBucket(RenderQueue.Bucket.Transparent);
tg.setQueueBucket(RenderQueue.Bucket.Gui);
tg.setMaterial(tm);
tg.setLocalTranslation(thumbX, thumbY, -16);
tg.setLocalTranslation(thumbX, thumbY, -16f);
parent.attachChild(tg);
} else {
quad(parent, thumbX, thumbY, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, -16);
addQuad(parent, thumbX, thumbY, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, -16);
}
// Anzahl-Badge unten rechts (nur wenn > 1)
if (count > 1) {
String cntStr = count > 999 ? "999+" : String.valueOf(count);
BitmapText cntTxt = txt(cntStr, 13, COL_WHITE);
BitmapText cntTxt = crispText(cntStr, UiTheme.FONT_SMALL, UiTheme.COL_WHITE);
float bw = Math.max(26, cntTxt.getLineWidth() + 8);
float bh = 18;
float bx = x + CELL_W - bw - 5;
float by = thumbY + 4;
quad(parent, bx, by, bw, bh, COL_BADGE, -15);
addQuad(parent, bx, by, bw, bh, COL_BADGE, -15f);
cntTxt.setLocalTranslation(bx + (bw - cntTxt.getLineWidth()) / 2f, by + bh - 3, -14);
parent.attachChild(cntTxt);
}
// Name
BitmapText nameTxt = txt(clip(item.getDisplayText(), 17), 13, COL_WHITE);
BitmapText nameTxt = crispText(clip(item.getDisplayText(), 17), UiTheme.FONT_SMALL, UiTheme.COL_WHITE);
nameTxt.setLocalTranslation(x + (CELL_W - nameTxt.getLineWidth()) / 2f, y + 42, -16);
parent.attachChild(nameTxt);
// Goldwert
BitmapText goldTxt = txt(item.getWorthGold() + " G", 12, COL_GOLD);
BitmapText goldTxt = crispText(item.getWorthGold() + " G", UiTheme.FONT_BODY, UiTheme.COL_GOLD);
goldTxt.setLocalTranslation(x + (CELL_W - goldTxt.getLineWidth()) / 2f, y + 22, -16);
parent.attachChild(goldTxt);
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
/** Lädt Texture oder gibt null zurück wenn nicht vorhanden. */
private Texture loadThumb(String assetPath) {
try { return assetManager.loadTexture(assetPath); }
try { return assets.loadTexture(assetPath); }
catch (Exception e) { return null; }
}
/** Items des Tabs sortiert nach SubKategorie-Ordinal, dann Preis. */
private List<Map.Entry<Item, Integer>> sortedItems(ItemCategory cat) {
Inventar inv = mc.getInventar();
if (inv == null) return List.of();
if (inv == null) { return List.of(); }
return inv.getItems().entrySet().stream()
.filter(e -> e.getKey().getCategory() == cat)
.sorted(Comparator
@@ -439,40 +370,6 @@ public class InventoryState extends BaseAppState {
.collect(Collectors.toList());
}
private float[] toVirtual(Vector2f screen) {
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
app.getCamera().getHeight() / MenuCanvas.REF_H);
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale };
}
private void destroyPanel() {
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
panel = null; gridNode = null;
}
private Geometry quad(Node parent, float x, float y, float w, float h, ColorRGBA col, float z) {
Geometry g = new Geometry("q", new Quad(w, h));
Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
m.setColor("Color", col.clone());
if (col.a < 1f) {
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
}
g.setMaterial(m);
g.setLocalTranslation(x, y, z);
parent.attachChild(g);
return g;
}
private BitmapText txt(String s, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font);
t.setSize(size); t.setColor(col); t.setText(s);
return t;
}
private static String clip(String s, int max) {
return s.length() <= max ? s : s.substring(0, max - 1) + "";
}

View File

@@ -0,0 +1,462 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node;
import de.blight.common.model.MainCharacter;
import de.blight.common.model.TextRegistry;
import de.blight.common.model.quests.*;
import de.blight.game.config.NinePatch;
import de.blight.game.config.OverlayState;
import de.blight.game.config.UiTheme;
import java.util.ArrayList;
import java.util.List;
/**
* Quest-Übersicht: offene Quests (links Liste, rechts Details) mit Tab für abgeschlossene.
* Öffnet/schließt mit Taste V oder ESC.
*/
public class QuestState extends OverlayState {
// ── Layout (innerhalb des OverlayState-Panels PX/PY/PW/PH) ──────────────
private static final float TAB_H = 32f;
private static final float LIST_W = 270f;
private static final float ITEM_H = 42f;
private static final float ITEM_GAP = 3f;
private static final float DETAIL_X = PX + PAD + LIST_W + 12f;
private static final float DETAIL_W = PW - PAD - LIST_W - 12f - PAD;
private static final float CONTENT_TOP = PY + PH - HDR_H - TAB_H - PAD;
private static final float CONTENT_BOTTOM = PY + PAD;
private static final float CONTENT_H = CONTENT_TOP - CONTENT_BOTTOM;
private static final int WRAP_CHARS = 48;
// ── Input-Mappings ────────────────────────────────────────────────────────
private static final String MAP_TOGGLE = "_QuestToggle";
private static final String MAP_SCROLL_UP = "_QuestScrollUp";
private static final String MAP_SCROLL_DN = "_QuestScrollDn";
private static final String MAP_CLICK = "_QuestClick";
// ── Daten ─────────────────────────────────────────────────────────────────
private final MainCharacter mc;
// ── UI-Zustand ────────────────────────────────────────────────────────────
private Node panel;
private Node listNode;
private Node detailNode;
private boolean showCompleted = false;
private int selectedIdx = 0;
private int scrollOffset = 0;
private float[][] tabBounds = new float[2][4];
private final List<float[]> itemBounds = new ArrayList<>();
public QuestState(MainCharacter mc) {
this.mc = mc;
}
// ── OverlayState-Implementierung ──────────────────────────────────────────
@Override
protected String getTitle() {
return "Questlog";
}
@Override
protected void initOverlayKeys() {
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(KeyInput.KEY_V));
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
app.getInputManager().addMapping(MAP_SCROLL_DN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
app.getInputManager().addMapping(MAP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addListener(toggleListener, MAP_TOGGLE);
}
@Override
protected void cleanupOverlayKeys() {
app.getInputManager().deleteMapping(MAP_TOGGLE);
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
app.getInputManager().deleteMapping(MAP_SCROLL_DN);
app.getInputManager().deleteMapping(MAP_CLICK);
}
@Override
protected void buildContent(Node parentPanel, float scale) {
this.panel = parentPanel;
// Tabs
buildTabs(parentPanel);
// Trennlinie Liste / Detail
float divX = PX + PAD + LIST_W + 6f;
addQuad(parentPanel, divX, CONTENT_BOTTOM, 1f, CONTENT_H, UiTheme.COL_DIVIDER, -17f);
listNode = new Node("quest-list");
detailNode = new Node("quest-detail");
parentPanel.attachChild(listNode);
parentPanel.attachChild(detailNode);
buildList();
buildDetail();
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void onEnable() {
selectedIdx = 0;
scrollOffset = 0;
super.onEnable();
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
app.getInputManager().addListener(clickListener, MAP_CLICK);
}
@Override
protected void onDisable() {
app.getInputManager().removeListener(scrollListener);
app.getInputManager().removeListener(clickListener);
panel = null;
listNode = null;
detailNode = null;
super.onDisable();
}
@Override
protected void cleanup(Application application) {
super.cleanup(application);
}
// ── Listener ──────────────────────────────────────────────────────────────
private final ActionListener toggleListener = (name, pressed, tpf) -> {
if (pressed) { setEnabled(!isEnabled()); }
};
private final AnalogListener scrollListener = (name, value, tpf) -> {
List<Quest> quests = activeList();
int maxScroll = Math.max(0, quests.size() - visibleSlots());
if (MAP_SCROLL_UP.equals(name)) { scrollOffset = Math.max(0, scrollOffset - 1); }
else { scrollOffset = Math.min(maxScroll, scrollOffset + 1); }
rebuildList();
};
private final ActionListener clickListener = (name, pressed, tpf) -> {
if (!pressed || panel == null) { return; }
float[] v = toVirtual(app.getInputManager().getCursorPosition());
for (int i = 0; i < 2; i++) {
float[] b = tabBounds[i];
if (v[0] >= b[0] && v[0] <= b[0]+b[2] && v[1] >= b[1] && v[1] <= b[1]+b[3]) {
boolean wantCompleted = (i == 1);
if (wantCompleted != showCompleted) {
showCompleted = wantCompleted;
selectedIdx = 0;
scrollOffset = 0;
rebuild();
}
return;
}
}
for (int i = 0; i < itemBounds.size(); i++) {
float[] b = itemBounds.get(i);
if (v[0] >= b[0] && v[0] <= b[0]+b[2] && v[1] >= b[1] && v[1] <= b[1]+b[3]) {
int newIdx = scrollOffset + i;
List<Quest> quests = activeList();
if (newIdx < quests.size() && newIdx != selectedIdx) {
selectedIdx = newIdx;
rebuildList();
rebuildDetail();
}
return;
}
}
};
// ── Tabs ──────────────────────────────────────────────────────────────────
private void buildTabs(Node panel) {
float tabY = PY + PH - HDR_H - TAB_H;
float tabW = (PW - 8f) / 2f;
float tabX0 = PX + 4f;
String[] labels = {"Offene Quests", "Abgeschlossen"};
for (int i = 0; i < 2; i++) {
boolean active = (i == 0 && !showCompleted) || (i == 1 && showCompleted);
float tx = tabX0 + i * tabW;
NinePatch patch = active ? NinePatch.tabActive(assets) : NinePatch.tabInactive(assets);
panel.attachChild(patch.build(tx, tabY, tabW - 4, TAB_H, -18f));
BitmapText lbl = crispText(labels[i], UiTheme.FONT_SMALL,
active ? UiTheme.COL_WHITE : UiTheme.COL_MUTED);
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
panel.attachChild(lbl);
tabBounds[i] = new float[]{tx, tabY, tabW - 4, TAB_H};
}
}
// ── Liste ─────────────────────────────────────────────────────────────────
private void buildList() {
itemBounds.clear();
List<Quest> quests = activeList();
float x = PX + PAD;
if (quests.isEmpty()) {
BitmapText empty = crispText(
showCompleted ? "Keine abgeschlossenen Quests." : "Keine offenen Quests.",
UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
empty.setLocalTranslation(x, CONTENT_TOP - 20, -17);
listNode.attachChild(empty);
return;
}
int slots = visibleSlots();
int end = Math.min(scrollOffset + slots, quests.size());
for (int i = scrollOffset; i < end; i++) {
Quest q = quests.get(i);
int slot = i - scrollOffset;
float iy = CONTENT_TOP - slot * (ITEM_H + ITEM_GAP) - ITEM_H;
boolean sel = (i == selectedIdx);
ColorRGBA bg = sel ? UiTheme.COL_SELECTED : UiTheme.COL_ITEM_BG;
addQuad(listNode, x, iy, LIST_W, ITEM_H, bg, -18);
String typeLabel = typeLabel(q);
ColorRGBA typeCol = typeColor(q);
BitmapText typeTxt = crispText(typeLabel, UiTheme.FONT_TINY, typeCol);
typeTxt.setLocalTranslation(x + 4, iy + ITEM_H - 5, -17);
listNode.attachChild(typeTxt);
String title = resolveTitle(q);
if (title.length() > 26) { title = title.substring(0, 25) + ""; }
BitmapText titleTxt = crispText(title, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
titleTxt.setLocalTranslation(x + 4, iy + ITEM_H - 18, -17);
listNode.attachChild(titleTxt);
if (q.getXp() > 0) {
BitmapText xpTxt = crispText(q.getXp() + " XP", UiTheme.FONT_TINY,
showCompleted ? UiTheme.COL_SUCCESS : UiTheme.COL_XP);
xpTxt.setLocalTranslation(x + LIST_W - xpTxt.getLineWidth() - 6, iy + 6, -17);
listNode.attachChild(xpTxt);
}
itemBounds.add(new float[]{x, iy, LIST_W, ITEM_H});
}
if (scrollOffset > 0) {
BitmapText up = crispText("", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
up.setLocalTranslation(x + LIST_W / 2f - 5, CONTENT_TOP + 2, -17);
listNode.attachChild(up);
}
if (scrollOffset + slots < quests.size()) {
BitmapText dn = crispText("", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
dn.setLocalTranslation(x + LIST_W / 2f - 5, CONTENT_BOTTOM - 2, -17);
listNode.attachChild(dn);
}
}
private void rebuildList() {
listNode.detachAllChildren();
itemBounds.clear();
buildList();
}
// ── Detail ────────────────────────────────────────────────────────────────
private void buildDetail() {
List<Quest> quests = activeList();
float x = DETAIL_X;
float y = CONTENT_TOP;
float w = DETAIL_W;
if (quests.isEmpty() || selectedIdx >= quests.size()) {
BitmapText nq = crispText("Keinen Quest ausgewählt.", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
nq.setLocalTranslation(x, y - 20, -17);
detailNode.attachChild(nq);
return;
}
Quest q = quests.get(selectedIdx);
String title = resolveTitle(q);
BitmapText titleTxt = crispText(title, UiTheme.FONT_HEADING, UiTheme.COL_GOLD);
titleTxt.setLocalTranslation(x, y, -17);
detailNode.attachChild(titleTxt);
y -= 24;
String typeLabel = typeLabel(q);
BitmapText typeTxt = crispText("[" + typeLabel + "]", UiTheme.FONT_BODY, typeColor(q));
typeTxt.setLocalTranslation(x, y, -17);
detailNode.attachChild(typeTxt);
if (q.getXp() > 0) {
ColorRGBA xpCol = showCompleted ? UiTheme.COL_SUCCESS : UiTheme.COL_XP;
BitmapText xpTxt = crispText("Belohnung: " + q.getXp() + " XP", UiTheme.FONT_BODY, xpCol);
xpTxt.setLocalTranslation(x + w - 130, y, -17);
detailNode.attachChild(xpTxt);
}
y -= 20;
addQuad(detailNode, x, y, w, 1f, UiTheme.COL_DIVIDER, -17f);
y -= 16;
String desc = TextRegistry.resolve(q.getDescription(), "");
if (desc != null && !desc.isBlank()) {
BitmapText dialogLbl = crispText("Dialog:", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
dialogLbl.setLocalTranslation(x, y, -17);
detailNode.attachChild(dialogLbl);
y -= 16;
List<String> lines = wrapText(desc, WRAP_CHARS);
float blockH = lines.size() * 15f + 10f;
addQuad(detailNode, x, y - blockH, w, blockH, UiTheme.COL_DIALOG_BG, -18f);
BitmapText q1 = crispText("\"", UiTheme.FONT_TITLE, UiTheme.COL_MUTED);
q1.setLocalTranslation(x + 6, y - 2, -17);
detailNode.attachChild(q1);
for (String line : lines) {
BitmapText lineTxt = crispText(line, UiTheme.FONT_BODY, UiTheme.COL_WHITE);
lineTxt.setLocalTranslation(x + 18, y - 4, -17);
detailNode.attachChild(lineTxt);
y -= 15;
}
BitmapText q2 = crispText("\"", UiTheme.FONT_TITLE, UiTheme.COL_MUTED);
q2.setLocalTranslation(x + w - 14, y - 2, -17);
detailNode.attachChild(q2);
y -= 18;
}
addQuad(detailNode, x, y, w, 1f, UiTheme.COL_DIVIDER, -17f);
y -= 16;
BitmapText aufgabeLbl = crispText("Aufgabe:", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
aufgabeLbl.setLocalTranslation(x, y, -17);
detailNode.attachChild(aufgabeLbl);
y -= 18;
String objective = resolveObjective(q);
for (String line : wrapText(objective, WRAP_CHARS)) {
BitmapText lineTxt = crispText(line, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
lineTxt.setLocalTranslation(x + 8, y, -17);
detailNode.attachChild(lineTxt);
y -= 17;
}
if (showCompleted) {
String successText = TextRegistry.resolve(q.getSuccessText(), "");
if (successText != null && !successText.isBlank()) {
y -= 8;
addQuad(detailNode, x, y, w, 1f, UiTheme.COL_DIVIDER, -17f);
y -= 16;
BitmapText abschlussLbl = crispText("Abschluss:", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
abschlussLbl.setLocalTranslation(x, y, -17);
detailNode.attachChild(abschlussLbl);
y -= 16;
for (String line : wrapText(successText, WRAP_CHARS)) {
BitmapText lineTxt = crispText(line, UiTheme.FONT_BODY, UiTheme.COL_COMPLETED);
lineTxt.setLocalTranslation(x + 8, y, -17);
detailNode.attachChild(lineTxt);
y -= 15;
}
}
}
}
private void rebuildDetail() {
detailNode.detachAllChildren();
buildDetail();
}
private void rebuild() {
setEnabled(false);
setEnabled(true);
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private List<Quest> activeList() {
if (mc == null) { return List.of(); }
List<Quest> src = showCompleted ? mc.getCompletedQuests() : mc.getOpenQuests();
return src != null ? src : List.of();
}
private int visibleSlots() {
return (int) (CONTENT_H / (ITEM_H + ITEM_GAP));
}
private static String resolveTitle(Quest q) {
String t = TextRegistry.resolve(q.getText(), q.getQuestId() != null ? q.getQuestId() : "");
return (t == null || t.isBlank()) ? (q.getQuestId() != null ? q.getQuestId() : "Unbekannt") : t;
}
private static String resolveObjective(Quest q) {
if (q instanceof TalkQuest tq && tq.getTalkTo() != null) {
return "Sprich mit " + tq.getTalkTo().getDisplayText();
}
if (q instanceof ItemQuest iq && iq.getItem() != null) {
return "Sammle " + iq.getCount() + "× " + iq.getItem().getDisplayText();
}
if (q instanceof BringQuest bq) {
String npc = bq.getBring() != null ? bq.getBring().getDisplayText() : "?";
String loc = bq.getBringTo() != null
? TextRegistry.resolve(bq.getBringTo().getName(), "?") : "?";
return "Bringe " + npc + " nach " + loc;
}
if (q instanceof FollowQuest fq) {
String npc = fq.getFollow() != null ? fq.getFollow().getDisplayText() : "?";
String loc = fq.getFollowTo() != null
? TextRegistry.resolve(fq.getFollowTo().getName(), "?") : "?";
return "Begleite " + npc + " nach " + loc;
}
if (q instanceof InteractQuest iq && iq.getInteractWith() != null) {
return "Interagiere mit " + iq.getInteractWith().getDisplayText();
}
return TextRegistry.resolve(q.getDescription(), "");
}
private static String typeLabel(Quest q) {
if (q instanceof TalkQuest) { return "Gespräch"; }
if (q instanceof ItemQuest) { return "Sammeln"; }
if (q instanceof BringQuest) { return "Bringen"; }
if (q instanceof FollowQuest) { return "Begleiten"; }
if (q instanceof InteractQuest) { return "Interaktion"; }
return "Quest";
}
private static ColorRGBA typeColor(Quest q) {
if (q instanceof TalkQuest) { return UiTheme.COL_TYPE_TALK; }
if (q instanceof ItemQuest) { return UiTheme.COL_TYPE_ITEM; }
if (q instanceof BringQuest) { return UiTheme.COL_TYPE_BRING; }
if (q instanceof FollowQuest) { return UiTheme.COL_TYPE_FOLLOW; }
if (q instanceof InteractQuest) { return UiTheme.COL_TYPE_INTERACT; }
return UiTheme.COL_MUTED;
}
private static List<String> wrapText(String text, int charsPerLine) {
List<String> result = new ArrayList<>();
if (text == null || text.isBlank()) { return result; }
String[] words = text.split("\\s+");
StringBuilder line = new StringBuilder();
for (String word : words) {
if (line.length() > 0 && line.length() + 1 + word.length() > charsPerLine) {
result.add(line.toString());
line = new StringBuilder();
}
if (line.length() > 0) { line.append(' '); }
line.append(word);
}
if (line.length() > 0) { result.add(line.toString()); }
return result;
}
}

View File

@@ -275,6 +275,7 @@ public class RiverState extends BaseAppState {
// ── Worley-Noise Schaum-Textur ────────────────────────────────────────────
@SuppressWarnings("deprecation")
private Texture2D generateFoamTexture() {
int size = 256;
int nPts = 40;
@@ -322,6 +323,7 @@ public class RiverState extends BaseAppState {
* Die Spray-Richtung ist das Spiegelbild der horizontalen Fließrichtung
* (Wasser prallt auf und spritzt zurück + hoch).
*/
@SuppressWarnings("deprecation")
private void buildWaterfallParticles(RiverPoint base, Vector3f flowDir) {
ParticleEmitter emitter = new ParticleEmitter(
"waterfall_particles", ParticleMesh.Type.Triangle, 60);

View File

@@ -284,7 +284,7 @@ public class SculptedMeshState extends BaseAppState {
buf.put((byte) rgb[0]).put((byte) rgb[1]).put((byte) rgb[2]).put((byte) 255);
buf.flip();
Texture2D tex = new Texture2D(
new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf));
new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf, ColorSpace.Linear));
tex.setWrap(Texture.WrapMode.Repeat);
return tex;
}