diff --git a/blight-game/src/main/java/de/blight/game/BlightGame.java b/blight-game/src/main/java/de/blight/game/BlightGame.java index 5e564af..27a21ab 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -23,6 +23,9 @@ import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWVidMode; import org.lwjgl.system.MemoryStack; import com.jme3.system.lwjgl.LwjglWindow; +import de.blight.game.state.HotbarState; +import de.blight.game.state.HudState; +import de.blight.game.state.LoadingScreenState; import javax.imageio.ImageIO; import java.nio.IntBuffer; @@ -50,14 +53,16 @@ public class BlightGame extends SimpleApplication { private AudioScreen audioScreen; private PauseMenu pauseMenu; private MainMenuState mainMenuState; + private LoadingScreenState loadingScreen; /** 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 long startTimeMs = 0; + private long lastFullscreenFix = 0; + private long pendingFullscreenFixAt = Long.MAX_VALUE; + private boolean firstLaunch = true; // false nach dem ersten simpleInitApp() + private boolean initialFixDone = false; // Initial-Fix bei 0.5s bereits gelaufen private JWindow splashWindow; @@ -101,6 +106,12 @@ public class BlightGame extends SimpleApplication { settings.setBitsPerPixel(32); settings.setVSync(gs.vsync); settings.setSamples(gs.samples); + if (!gs.fullscreen) { + // Fenster beim Start auf dem Primär-Monitor zentrieren. + // Ohne diesen Hint öffnet GLFW das Fenster bei (0,0) oder wo der WM will – + // bei Editor-Start typisch auf dem Sekundär-Monitor. + settings.setCenterWindow(true); + } // Nur unter X11-Linux: GLFW auf X11-Backend festlegen, damit libdecor keine // Fensterrahmen im Vollbild hinzufügt. Unter nativem Wayland nicht setzen. if ("x11".equalsIgnoreCase(System.getenv("XDG_SESSION_TYPE"))) { @@ -188,14 +199,23 @@ public class BlightGame extends SimpleApplication { audioSettings = AudioSettingsStore.load(); startTimeMs = System.currentTimeMillis(); + initialFixDone = false; 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(); + log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={} gewünschte Aufl.={}×{}", + cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen, + graphicsSettings.width, graphicsSettings.height); + if (firstLaunch) { + pendingFullscreenFixAt = Long.MAX_VALUE; + // Fenster NICHT verstecken: der Toggle windowed→fullscreen in fixFullscreenMonitor() + // funktioniert nur zuverlässig, wenn das Fenster bereits in einem aktiven + // Fullscreen-Zustand war (wie beim 5s-Watchdog). Aus dem Hidden-Zustand behandelt + // Cinnamon den nächsten Fullscreen-Übergang als Neuerstplatzierung und ignoriert + // den Ziel-Monitor zugunsten des aktiven Workspaces (Editor-Monitor). + } + // Restart nach Einstellungsänderung: pendingFullscreenFixAt (scheduleFullscreenFix) + // bleibt gesetzt und übernimmt den Fix nach 1 s. } + firstLaunch = false; stateManager.attach(new AudioSettingsState(audioSettings)); status("Lade Spielstand..."); @@ -207,6 +227,12 @@ public class BlightGame extends SimpleApplication { worldScene.setEnabled(false); stateManager.attach(worldScene); + // Ladebildschirm – wird von startWorld() aktiviert und löst worldScene.setEnabled(true) + // erst nach einem gerenderten Frame aus (kein schwarzer Startbildschirm). + loadingScreen = new LoadingScreenState(); + loadingScreen.setEnabled(false); + stateManager.attach(loadingScreen); + // screenCloseTarget: dynamisch gesetzt, je nachdem ob Config/Grafik vom Hauptmenü // oder vom Pausemenü geöffnet wurde. screenCloseTarget = () -> {}; @@ -388,8 +414,16 @@ public class BlightGame extends SimpleApplication { if (mainMenuState != null) { mainMenuState.setEnabled(false); } - worldScene.setEnabled(true); inputManager.setCursorVisible(false); + loadingScreen.setEnabled(true); + // LoadingScreenState löst worldScene.setEnabled(true) nach dem ersten gerenderten + // Ladebildschirm-Frame aus, damit der Spieler keinen schwarzen Bildschirm sieht. + } + + /** Wird von GraphicsScreen aufgerufen bevor app.restart() – sorgt dafür, dass + * fixFullscreenMonitor() ~1 s nach dem Neustart läuft (WM braucht Zeit zum Stabilisieren). */ + public void scheduleFullscreenFix() { + pendingFullscreenFixAt = System.currentTimeMillis() + 1000; } // ── Render-Loop ────────────────────────────────────────────────────────── @@ -400,11 +434,26 @@ 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. + long now = System.currentTimeMillis(); + + // Initial-Fix: 200 ms nach dem ersten Start. Fenster war die ganze Zeit sichtbar + // (kein glfwHideWindow) → Cinnamon kennt es als "existing fullscreen window" → + // Toggle windowed→fullscreen respektiert Ziel-Monitor (identisch zum Watchdog-Pfad). + if (!initialFixDone && graphicsSettings != null && graphicsSettings.fullscreen + && now - startTimeMs > 200) { + initialFixDone = true; + lastFullscreenFix = now; + fixFullscreenMonitor(); + } + + if (now >= pendingFullscreenFixAt) { + pendingFullscreenFixAt = Long.MAX_VALUE; + lastFullscreenFix = now; + fixFullscreenMonitor(); + } + + // Watchdog: Vollbild-Verlust durch GNOME/WM (erkennbar an Auflösungs-Mismatch). if (graphicsSettings != null && graphicsSettings.fullscreen) { - long now = System.currentTimeMillis(); if (now - startTimeMs > 5_000 && (cam.getWidth() != graphicsSettings.width || cam.getHeight() != graphicsSettings.height) @@ -417,6 +466,28 @@ public class BlightGame extends SimpleApplication { } } + @Override + public void reshape(int w, int h) { + super.reshape(w, h); + if (gameReady) { + rebuildScreenDependentStates(); + } + } + + private void rebuildScreenDependentStates() { + rebuildState(HudState.class); + rebuildState(HotbarState.class); + rebuildState(LoadingScreenState.class); + } + + private void rebuildState(Class cls) { + T state = stateManager.getState(cls); + if (state != null && state.isEnabled()) { + state.setEnabled(false); + state.setEnabled(true); + } + } + // ── Konsolen-Befehle ───────────────────────────────────────────────────── private void registerGameCommands(JmeConsole console) { @@ -482,6 +553,14 @@ public class BlightGame extends SimpleApplication { } // Vollbild sicherstellen: GLFW ist hier bereits durch JME3 initialisiert. + /** Thread.sleep + glfwPollEvents: garantiertes Zeitfenster für WM-Reaktionen. + * glfwWaitEventsTimeout() kehrt beim ersten Event zurück, nicht nach voller Zeit. */ + private static void glfwSleep(long ms) { + GLFW.glfwPollEvents(); + try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + GLFW.glfwPollEvents(); + } + // Problem: JME3 erstellt das Fenster sofort sichtbar. Cinnamon/Muffin (GLFW 3.5 nutzt // _NET_WM_STATE_FULLSCREEN statt OverrideRedirect) schiebt das Fenster beim ersten // Erscheinen um eine Panel-Höhe nach unten (z.B. Y+32), bevor der WM den State @@ -503,34 +582,20 @@ public class BlightGame extends SimpleApplication { // Ggf. iconifiziertes Fenster wiederherstellen GLFW.glfwRestoreWindow(win); - // Ist das Fenster bereits in GLFW-Vollbild (OverrideRedirect)? + long target = GLFW.glfwGetPrimaryMonitor(); 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 (mons != null) { + 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=({},{}), primary={}, current={}", + i, mx.get(0), my.get(0), + (target == mons.get(i)) ? "JA" : "nein", + (currentMonitor == mons.get(i)) ? "JA" : "nein"); } } } @@ -541,16 +606,39 @@ public class BlightGame extends SimpleApplication { return; } - // Windowed → Fullscreen Toggle: - // JME3 erstellt das Fenster sofort sichtbar; Cinnamon setzt Y+32 (Panel-Offset) - // bevor _NET_WM_STATE_FULLSCREEN vollständig verarbeitet ist. - // Kurz in den Fenstermodus wechseln und zurück → WM verarbeitet den - // State neu und setzt Y korrekt auf Monitor-Origin. - log.info("[Grafik] Vollbild-Fix: Toggle Fenstermodus → Vollbild auf Monitor {} ({}×{}@{}Hz)", - target, vm.width(), vm.height(), vm.refreshRate()); - GLFW.glfwSetWindowMonitor(win, 0L, awt.x, awt.y, vm.width(), vm.height(), 0); - GLFW.glfwWaitEventsTimeout(0.1); - GLFW.glfwSetWindowMonitor(win, target, 0, 0, vm.width(), vm.height(), vm.refreshRate()); + int fsW = (graphicsSettings != null && graphicsSettings.width > 0) ? graphicsSettings.width : vm.width(); + int fsH = (graphicsSettings != null && graphicsSettings.height > 0) ? graphicsSettings.height : vm.height(); + + // Primärmonitor-Position für den Windowed-Schritt + int primX = 0, primY = 0; + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer px = stack.mallocInt(1); + IntBuffer py = stack.mallocInt(1); + GLFW.glfwGetMonitorPos(target, px, py); + primX = px.get(0); + primY = py.get(0); + } + + // Fenster VOR dem Toggle anzeigen und fokussieren. + // Cinnamon respektiert beim windowed-Schritt die angeforderte Position nur dann, + // wenn das Fenster fokussiert ist. Ohne Fokus verschiebt Cinnamon das Fenster + // auf den aktiven Workspace (Editor-Monitor) → der Fullscreen-Schritt danach + // landet dann ebenfalls auf dem falschen Monitor. + GLFW.glfwShowWindow(win); + GLFW.glfwFocusWindow(win); + glfwSleep(100); // WM-Fokuswechsel abwarten + + // Toggle Vollbild → Fenster → Vollbild auf Primär-Monitor. + log.info("[Grafik] Vollbild-Fix: Toggle → Vollbild {}×{} auf Monitor {} (@{}Hz)", + fsW, fsH, target, vm.refreshRate()); + GLFW.glfwSetWindowMonitor(win, 0L, primX, primY, fsW, fsH, 0); + // Cursor auf Zielmonitor warpen: Muffin/Cinnamon nutzt meta_screen_get_current_monitor() + // (= Cursor-Position), nicht die Fensterposition, für Fullscreen-Placement. + // glfwSetCursorPos ist relativ zum Fenster → Mitte des Fensters = Mitte des Zielmonitors. + GLFW.glfwSetCursorPos(win, fsW / 2.0, fsH / 2.0); + glfwSleep(80); + GLFW.glfwSetWindowMonitor(win, target, 0, 0, fsW, fsH, vm.refreshRate()); + glfwSleep(200); GLFW.glfwFocusWindow(win); } catch (Throwable e) { diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java index ffb6028..e406a98 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java @@ -4,14 +4,22 @@ import com.jme3.font.BitmapText; import com.jme3.math.ColorRGBA; import com.jme3.scene.Node; import com.jme3.system.AppSettings; +import com.jme3.system.lwjgl.LwjglWindow; +import de.blight.game.BlightGame; import de.blight.game.state.TerrainChunkState; import de.blight.game.state.WeatherState; import de.blight.lang.TextResolver; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWVidMode; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; public class GraphicsScreen extends MenuScreen { - private static final int[][] RESOLUTIONS = { - {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160} + private static final int[][] FALLBACK_RESOLUTIONS = { + {1024, 768}, {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160} }; private static final int[] SAMPLES = {0, 2, 4, 8}; @@ -27,6 +35,7 @@ public class GraphicsScreen extends MenuScreen { private GraphicsSettings edit; private final Runnable onClose; + private int[][] resolutions = FALLBACK_RESOLUTIONS; private int resIdx; private int samplesIdx; @@ -44,6 +53,8 @@ public class GraphicsScreen extends MenuScreen { @Override protected void onEnableExtras() { + resolutions = buildResolutionList(); + edit = new GraphicsSettings(); edit.width = live.width; edit.height = live.height; edit.fullscreen = live.fullscreen; @@ -53,8 +64,8 @@ public class GraphicsScreen extends MenuScreen { edit.shadowQuality = live.shadowQuality; resIdx = 0; - for (int i = 0; i < RESOLUTIONS.length; i++) { - if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) { + for (int i = 0; i < resolutions.length; i++) { + if (resolutions[i][0] == edit.width && resolutions[i][1] == edit.height) { resIdx = i; break; } } @@ -118,7 +129,7 @@ public class GraphicsScreen extends MenuScreen { private void refreshText(int row) { String val = switch (row) { - case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1]; + case ROW_RES -> resolutions[resIdx][0] + "x" + resolutions[resIdx][1]; 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 @@ -148,9 +159,9 @@ public class GraphicsScreen extends MenuScreen { private void cycleRow(int row, int dir) { switch (row) { case ROW_RES: - resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length; - edit.width = RESOLUTIONS[resIdx][0]; - edit.height = RESOLUTIONS[resIdx][1]; + resIdx = (resIdx + dir + resolutions.length) % resolutions.length; + edit.width = resolutions[resIdx][0]; + edit.height = resolutions[resIdx][1]; break; case ROW_FULL: edit.fullscreen = !edit.fullscreen; break; case ROW_VSYNC: edit.vsync = !edit.vsync; break; @@ -204,6 +215,9 @@ public class GraphicsScreen extends MenuScreen { s.setVSync(live.vsync); s.setSamples(live.samples); app.setSettings(s); + if (live.fullscreen) { + ((BlightGame) getApplication()).scheduleFullscreenFix(); + } app.restart(); } } @@ -213,5 +227,41 @@ public class GraphicsScreen extends MenuScreen { if (onClose != null) onClose.run(); } + private int[][] buildResolutionList() { + try { + com.jme3.system.JmeContext ctx = getApplication().getContext(); + if (!(ctx instanceof LwjglWindow)) { + return FALLBACK_RESOLUTIONS; + } + long monitor = GLFW.glfwGetPrimaryMonitor(); + if (monitor == 0L) { + return FALLBACK_RESOLUTIONS; + } + GLFWVidMode.Buffer modes = GLFW.glfwGetVideoModes(monitor); + if (modes == null || modes.limit() == 0) { + return FALLBACK_RESOLUTIONS; + } + // Eindeutige (w, h) Paare sammeln — Duplikate (gleiche Auflösung, anderer Refresh) entfernen + LinkedHashSet seen = new LinkedHashSet<>(); + List list = new ArrayList<>(); + for (int i = 0; i < modes.limit(); i++) { + modes.position(i); + int w = modes.width(); + int h = modes.height(); + long key = ((long) w << 32) | (h & 0xFFFFFFFFL); + if (w >= 1024 && h >= 600 && seen.add(key)) { + list.add(new int[]{w, h}); + } + } + if (list.isEmpty()) { + return FALLBACK_RESOLUTIONS; + } + list.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1])); + return list.toArray(new int[0][]); + } catch (Throwable e) { + return FALLBACK_RESOLUTIONS; + } + } + private static String t(String id) { return TextResolver.get().resolveId(id); } } diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index fbaed7f..3bc1a41 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -371,9 +371,19 @@ public class WorldScene extends BaseAppState { new JobProgressAdapter() { @Override public void done(LightProbe result) { dayNight.getDayTime().setTimeOfDay(restoreTime); - app.getStateManager().detach(envCam); - envCam = null; - log.info("[WorldScene] LightProbe fertig, Uhrzeit zurück auf {}.", restoreTime); + // 2 Frames warten: JME3 braucht 1-2 Frames um die neue + // Probe auf die GPU zu übertragen. isGameReady() prüft + // envCam == null, daher erst dann setzen wenn die Probe + // für den Renderer sichtbar ist. + app.enqueue(() -> { + app.enqueue(() -> { + app.getStateManager().detach(envCam); + envCam = null; + log.info("[WorldScene] LightProbe fertig, Uhrzeit zurück auf {}.", restoreTime); + return null; + }); + return null; + }); } }); } @@ -1391,6 +1401,10 @@ public class WorldScene extends BaseAppState { * Teleportiert den Spieler zu (x, y, z). Wenn y == Float.NaN wird die * Terrainhöhe an (x, z) verwendet und ein kleiner Offset nach oben addiert. */ + public boolean isGameReady() { + return !physicsCharPending && animContextReady && envCam == null; + } + public String teleportPlayer(float x, float y, float z) { if (physicsChar == null) return "Spieler noch nicht initialisiert"; if (Float.isNaN(y)) { diff --git a/blight-game/src/main/java/de/blight/game/state/HudState.java b/blight-game/src/main/java/de/blight/game/state/HudState.java index 33465d9..08077c0 100644 --- a/blight-game/src/main/java/de/blight/game/state/HudState.java +++ b/blight-game/src/main/java/de/blight/game/state/HudState.java @@ -72,26 +72,26 @@ public class HudState extends BaseAppState { protected void initialize(Application application) { app = (SimpleApplication) application; ensureAssets(); + } + + @Override + protected void cleanup(Application application) {} + + @Override + protected void onEnable() { hudNode = new Node("hud_bars"); + fills[0] = fills[1] = fills[2] = null; + lastRatios[0] = lastRatios[1] = lastRatios[2] = -1f; buildBars(); app.getGuiNode().attachChild(hudNode); } - @Override - protected void cleanup(Application application) { - if (hudNode.getParent() != null) { - app.getGuiNode().detachChild(hudNode); - } - } - - @Override - protected void onEnable() { - hudNode.setCullHint(Spatial.CullHint.Inherit); - } - @Override protected void onDisable() { - hudNode.setCullHint(Spatial.CullHint.Always); + if (hudNode != null) { + app.getGuiNode().detachChild(hudNode); + hudNode = null; + } } @Override diff --git a/blight-game/src/main/java/de/blight/game/state/LoadingScreenState.java b/blight-game/src/main/java/de/blight/game/state/LoadingScreenState.java new file mode 100644 index 0000000..ec0ab7d --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/LoadingScreenState.java @@ -0,0 +1,185 @@ +package de.blight.game.state; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +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.texture.Texture2D; +import com.jme3.ui.Picture; +import de.blight.game.scene.WorldScene; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Zeigt Logo + Ladebalken, bis WorldScene spielbereit ist. + * + * Timing-Garantie: WorldScene.setEnabled(true) wird erst aufgerufen, nachdem + * mindestens ein vollständiger Render-Frame mit dem Overlay stattgefunden hat. + * Dazu wird app.enqueue() genutzt: Der eingereihte Task läuft zu Beginn des + * NÄCHSTEN Application.update()-Aufrufs – also nach dem aktuellen Render-Frame. + */ +public class LoadingScreenState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(LoadingScreenState.class); + + private static final float PROGRESS_SPEED = 0.18f; + private static final float TIMEOUT_SEC = 60f; + + private SimpleApplication app; + private Node overlayNode; + private Geometry barFill; + + private boolean readyToTrigger = false; + private boolean worldTriggered = false; + private float progress = 0f; + private float timeWaiting = 0f; + + @Override + protected void initialize(Application application) { + app = (SimpleApplication) application; + } + + @Override + protected void cleanup(Application application) {} + + @Override + protected void onEnable() { + if (!worldTriggered) { + readyToTrigger = false; + progress = 0f; + timeWaiting = 0f; + // Trigger erst im nächsten Frame einplanen: so wird der Overlay + // garantiert einmal gerendert, bevor WorldScene.onEnable() blockiert. + app.enqueue((java.util.concurrent.Callable) () -> { + if (!worldTriggered) { + readyToTrigger = true; + } + return null; + }); + } + buildOverlay(); + } + + @Override + protected void onDisable() { + if (overlayNode != null) { + app.getGuiNode().detachChild(overlayNode); + overlayNode = null; + barFill = null; + } + } + + @Override + public void update(float tpf) { + if (!worldTriggered && readyToTrigger) { + readyToTrigger = false; + worldTriggered = true; + WorldScene ws = app.getStateManager().getState(WorldScene.class); + if (ws != null) { + ws.setEnabled(true); + } + } + + timeWaiting += tpf; + WorldScene ws = app.getStateManager().getState(WorldScene.class); + boolean done = (ws != null && ws.isGameReady()) || timeWaiting >= TIMEOUT_SEC; + + if (!done) { + progress = Math.min(progress + tpf * PROGRESS_SPEED, 0.9f); + } else { + progress = 1.0f; + } + + if (barFill != null) { + barFill.setLocalScale(Math.max(0.001f, progress), 1f, 1f); + } + + if (done) { + setEnabled(false); + } + } + + // ── Aufbau ─────────────────────────────────────────────────────────────────── + + private void buildOverlay() { + if (overlayNode != null) { + app.getGuiNode().detachChild(overlayNode); + } + float sw = app.getCamera().getWidth(); + float sh = app.getCamera().getHeight(); + + overlayNode = new Node("loading_overlay"); + + // Schwarzer Vollbild-Hintergrund (Z=200: liegt garantiert über allen HUD-Elementen) + overlayNode.attachChild(makeRect(sw, sh, ColorRGBA.Black, 200f)); + + // Blight-Logo + buildLogo(sw, sh); + + // Ladebalken + buildBar(sw, sh); + + app.getGuiNode().attachChild(overlayNode); + } + + private void buildLogo(float sw, float sh) { + try { + Texture2D tex = (Texture2D) app.getAssetManager().loadTexture("logo.png"); + int imgW = tex.getImage().getWidth(); + int imgH = tex.getImage().getHeight(); + float logoW = sw * 0.40f; + float logoH = (imgH > 0 && imgW > 0) ? logoW * imgH / imgW : logoW * 0.27f; + Picture logo = new Picture("loading_logo"); + logo.setTexture(app.getAssetManager(), tex, true); + logo.setWidth(logoW); + logo.setHeight(logoH); + logo.setLocalTranslation((sw - logoW) / 2f, sh * 0.42f, 202f); + logo.setQueueBucket(RenderQueue.Bucket.Gui); + overlayNode.attachChild(logo); + } catch (Exception e) { + log.warn("[LoadingScreen] logo.png nicht ladbar: {}", e.getMessage()); + } + } + + private void buildBar(float sw, float sh) { + float bw = sw * 0.50f; + float bh = 16f; + float bx = (sw - bw) / 2f; + float by = sh * 0.14f; + float border = 2f; + + Geometry barBorder = makeRect(bw, bh, new ColorRGBA(0.45f, 0.35f, 0.08f, 1f), 1f); + barBorder.setLocalTranslation(bx, by, 201f); + overlayNode.attachChild(barBorder); + + Geometry barBg = makeRect(bw - border * 2, bh - border * 2, + new ColorRGBA(0.08f, 0.08f, 0.08f, 1f), 1f); + barBg.setLocalTranslation(bx + border, by + border, 201f); + overlayNode.attachChild(barBg); + + // Füllbalken: X-Scale animiert die Breite von links nach rechts + barFill = makeRect(bw - border * 2, bh - border * 2, + new ColorRGBA(0.85f, 0.65f, 0.10f, 1f), 2f); + barFill.setLocalTranslation(bx + border, by + border, 202f); + barFill.setLocalScale(Math.max(0.001f, progress), 1f, 1f); + overlayNode.attachChild(barFill); + } + + private Geometry makeRect(float w, float h, ColorRGBA color, float z) { + Quad quad = new Quad(w, h); + Geometry g = new Geometry("loading_rect", quad); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + g.setMaterial(mat); + g.setQueueBucket(RenderQueue.Bucket.Gui); + g.setLocalTranslation(0, 0, z); + return g; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java index 6ae5992..be87b6e 100644 --- a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java +++ b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java @@ -172,13 +172,21 @@ public class TerrainChunkState extends BaseAppState { @Override public void update(float tpf) { - float refX, refZ; + Vector3f camPos = cam.getLocation(); + float refX = camPos.x; + float refZ = camPos.z; if (!Float.isNaN(spawnHintX)) { - refX = spawnHintX; refZ = spawnHintZ; - spawnHintX = Float.NaN; spawnHintZ = Float.NaN; - } else { - Vector3f camPos = cam.getLocation(); - refX = camPos.x; refZ = camPos.z; + int spawnCx = worldToChunk(spawnHintX); + int spawnCz = worldToChunk(spawnHintZ); + if (worldToChunk(refX) != spawnCx || worldToChunk(refZ) != spawnCz) { + // Kamera noch nicht am Spawn-Chunk — Hint beibehalten + refX = spawnHintX; + refZ = spawnHintZ; + } else { + // Kamera hat den Spawn-Chunk erreicht — Hint freigeben + spawnHintX = Float.NaN; + spawnHintZ = Float.NaN; + } } int pcx = worldToChunk(refX); int pcz = worldToChunk(refZ); diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index 58f41a1..dbb2312 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -58,6 +58,7 @@ menu.graphics.row.shadow=Schattenqualität menu.graphics.val.shadow.low=Niedrig menu.graphics.val.shadow.medium=Mittel menu.graphics.val.shadow.high=Hoch +menu.graphics.val.resolution.native=Nativ menu.graphics.btn.apply=Übernehmen menu.graphics.btn.cancel=Abbrechen diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index 98a1174..a06fe7e 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -61,6 +61,7 @@ menu.graphics.row.shadow=Shadow Quality menu.graphics.val.shadow.low=Low menu.graphics.val.shadow.medium=Medium menu.graphics.val.shadow.high=High +menu.graphics.val.resolution.native=Native menu.graphics.btn.apply=Apply menu.graphics.btn.cancel=Cancel diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 1530377..0b9fbb3 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ