Weiter am Wettersystem gearbeitet, ordentliche Wolken und Regen etc ergänzt

This commit is contained in:
2026-07-24 11:41:05 +02:00
parent 5a7d9897e4
commit 919f70bb44
47 changed files with 1665 additions and 1073 deletions

View File

@@ -0,0 +1,139 @@
package de.blight.game;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* Globaler Exception-Handler.
*
* Kritisch (JME-Render-Thread via BlightGame.handleError):
* GLFW-Cursor wird vor dem Dialog freigegeben. Blockierender Dialog → OK → System.exit(1).
* Das JME-Fenster bleibt als eingefrorenem Hintergrund sichtbar, bis der Prozess endet.
*
* Nicht kritisch (Hintergrundthreads via UncaughtExceptionHandler):
* Cursor wird über registrierte Callbacks temporär eingeblendet, nach OK wieder versteckt.
* Das Spiel läuft während des Dialogs weiter.
*/
public final class BlightExceptionHandler implements Thread.UncaughtExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(BlightExceptionHandler.class);
/** Zeigt den Mauszeiger (auf dem JME-Thread via enqueue auszuführen). */
private static Runnable showCursorCb;
/** Versteckt den Mauszeiger wieder (auf dem JME-Thread via enqueue auszuführen). */
private static Runnable hideCursorCb;
private BlightExceptionHandler() {}
public static void install() {
Thread.setDefaultUncaughtExceptionHandler(new BlightExceptionHandler());
}
/**
* Cursor-Callbacks registrieren. Wird von BlightGame.simpleInitApp() aufgerufen,
* sobald GLFW und InputManager bereit sind.
*/
public static void registerCursorCallbacks(Runnable showCursor, Runnable hideCursor) {
showCursorCb = showCursor;
hideCursorCb = hideCursor;
}
// ── Hintergrundthread-Fehler (nicht kritisch) ─────────────────────────────
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("[Exception] Uncaught in '{}': {}", t.getName(), e.getMessage(), e);
// Cursor einblenden, bevor der Dialog auf dem EDT erscheint
if (showCursorCb != null) showCursorCb.run();
EventQueue.invokeLater(() -> {
showDialog(t.getName(), e, false);
// Nach Schließen des Dialogs Cursor wieder einfangen
if (hideCursorCb != null) hideCursorCb.run();
});
}
// ── JME-Render-Thread-Fehler (kritisch) ──────────────────────────────────
/**
* Zeigt einen blockierenden Dialog und beendet die Anwendung nach OK.
* Muss erst NACH dem GLFW-Cursor-Release aufgerufen werden.
*/
public static void showCritical(String threadName, Throwable e) {
log.error("[Exception] Kritisch in '{}': {}", threadName, e.getMessage(), e);
try {
if (EventQueue.isDispatchThread()) {
showDialog(threadName, e, true);
} else {
EventQueue.invokeAndWait(() -> showDialog(threadName, e, true));
}
} catch (Exception ex) {
log.error("[Exception] Fehlerdialog konnte nicht angezeigt werden", ex);
System.exit(1);
}
}
// ── Dialog ────────────────────────────────────────────────────────────────
private static void showDialog(String threadName, Throwable e, boolean critical) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
JDialog dialog = new JDialog((Frame) null,
critical ? "Kritischer Fehler" : "Fehler", true);
dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
dialog.setLayout(new BorderLayout(8, 8));
JLabel titleLbl = new JLabel(critical
? "Kritischer Fehler Anwendung wird beendet"
: "Fehler in Hintergrundthread Spiel läuft weiter");
titleLbl.setFont(titleLbl.getFont().deriveFont(Font.BOLD, 14f));
titleLbl.setForeground(critical ? new Color(0xcc2200) : new Color(0x884400));
titleLbl.setBorder(BorderFactory.createEmptyBorder(10, 14, 4, 14));
String msg = e.getClass().getName()
+ (e.getMessage() != null ? ": " + e.getMessage() : "")
+ " [Thread: " + threadName + "]";
JLabel infoLbl = new JLabel(msg);
infoLbl.setFont(infoLbl.getFont().deriveFont(Font.PLAIN, 12f));
infoLbl.setBorder(BorderFactory.createEmptyBorder(0, 14, 10, 14));
JPanel north = new JPanel(new GridLayout(2, 1));
north.add(titleLbl);
north.add(infoLbl);
JTextArea ta = new JTextArea(sw.toString(), 22, 90);
ta.setEditable(false);
ta.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
JScrollPane scroll = new JScrollPane(ta);
scroll.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8));
JButton btn = new JButton(critical ? "Anwendung beenden" : "OK");
btn.setFont(btn.getFont().deriveFont(Font.BOLD, 13f));
JPanel south = new JPanel(new FlowLayout(FlowLayout.CENTER));
south.add(btn);
dialog.add(north, BorderLayout.NORTH);
dialog.add(scroll, BorderLayout.CENTER);
dialog.add(south, BorderLayout.SOUTH);
dialog.setSize(920, 560);
dialog.setLocationRelativeTo(null);
Runnable close = () -> {
dialog.dispose();
if (critical) System.exit(1);
};
btn.addActionListener(ev -> close.run());
dialog.addWindowListener(new WindowAdapter() {
@Override public void windowClosing(WindowEvent ev) { close.run(); }
});
dialog.setVisible(true); // blockiert (modal) bis der Dialog geschlossen wird
}
}

View File

@@ -77,6 +77,7 @@ public class BlightGame extends SimpleApplication {
public static void main(String[] args) {
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
BlightExceptionHandler.install();
BlightGame app = new BlightGame();
app.splashWindow = showSplash();
@@ -203,6 +204,17 @@ public class BlightGame extends SimpleApplication {
startTimeMs = System.currentTimeMillis();
initialFixDone = false;
// Cursor-Callbacks für nicht-kritische Exception-Dialoge aus Hintergrundthreads
BlightExceptionHandler.registerCursorCallbacks(
() -> enqueue(() -> {
if (context instanceof LwjglWindow) {
long win = ((LwjglWindow) context).getWindowHandle();
if (win != 0L) GLFW.glfwSetInputMode(win, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
}
}),
() -> enqueue(() -> inputManager.setCursorVisible(false))
);
if (graphicsSettings.fullscreen) {
log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={} gewünschte Aufl.={}×{}",
cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen,
@@ -375,6 +387,8 @@ public class BlightGame extends SimpleApplication {
worldScene.setPaused(true);
}, "ToggleMenu");
stateManager.attach(new de.blight.game.state.CursorState());
// ── Startentscheidung: Hauptmenü oder direkt ins Spiel (Editor-Start) ─
boolean autostart = Boolean.getBoolean("blight.autostart");
if (autostart) {
@@ -384,6 +398,24 @@ public class BlightGame extends SimpleApplication {
}
}
@Override
public void handleError(String errMsg, Throwable t) {
// GLFW-Cursor freigeben, damit der Dialog bedienbar ist.
// Das JME-Fenster bleibt als eingefrorener Hintergrund sichtbar bis System.exit().
try {
if (context instanceof LwjglWindow) {
long win = ((LwjglWindow) context).getWindowHandle();
if (win != 0L) {
GLFW.glfwSetInputMode(win, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
}
}
} catch (Exception ignored) {}
Throwable ex = t != null ? t : new RuntimeException(errMsg);
BlightExceptionHandler.showCritical(Thread.currentThread().getName(), ex);
stop(); // Fallback: wird nur erreicht wenn der Dialog nicht angezeigt werden konnte
}
// ── Start-Flows ──────────────────────────────────────────────────────────
private void showMainMenu() {
@@ -568,14 +600,44 @@ public class BlightGame extends SimpleApplication {
if (ws == null) return "Wettersystem nicht aktiv";
if (args.length < 2)
return "Aktuell: " + ws.getActiveWeather()
+ " | Syntax: weather <sunny|cloudy|overcast|storm>";
+ " | Syntax: weather current | weather next | weather <"
+ java.util.Arrays.stream(de.blight.game.state.WeatherState.Weather.values())
.map(w -> w.name().toLowerCase())
.collect(java.util.stream.Collectors.joining("|"))
+ ">";
if ("current".equalsIgnoreCase(args[1])) {
float windDeg = (float) Math.toDegrees(ws.getWindAngle());
if (windDeg < 0) windDeg += 360f;
String[] compass = {"N","NO","O","SO","S","SW","W","NW"};
String dir = compass[Math.round(windDeg / 45f) % 8];
return String.format(
"Wetter: %s%n" +
"Wind: %.1f km/h aus %s (%.0f°)%n" +
"Regen: %.0f%%%n" +
"Wolken: %.0f%%%n" +
"Nebel: %.3f%n" +
"Nächste Ä.: in %.0f s",
ws.getActiveWeather(),
ws.getWindSpeed(), dir, windDeg,
ws.getRainIntensity() * 100f,
ws.getCloudCover() * 100f,
ws.getFogDensity(),
ws.getChangeTimer());
}
if ("next".equalsIgnoreCase(args[1])) {
de.blight.game.state.WeatherState.Weather next = ws.triggerNext();
return "Wetter → " + next;
}
try {
de.blight.game.state.WeatherState.Weather w =
de.blight.game.state.WeatherState.Weather.valueOf(args[1].toUpperCase());
ws.forceWeather(w);
return "Wetter gesetzt: " + w;
} catch (IllegalArgumentException e) {
return "Unbekanntes Wetter. Erlaubt: sunny, cloudy, overcast, storm";
return "Unbekanntes Wetter. Erlaubt: next, "
+ java.util.Arrays.stream(de.blight.game.state.WeatherState.Weather.values())
.map(w -> w.name().toLowerCase())
.collect(java.util.stream.Collectors.joining(", "));
}
});

View File

@@ -438,6 +438,8 @@ public class WorldScene extends BaseAppState {
ColorRGBA ac = dayNight.getCustomAmbient();
terrainMaterial.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b));
terrainMaterial.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b));
de.blight.game.state.RainState rs = app.getStateManager().getState(de.blight.game.state.RainState.class);
if (rs != null) terrainMaterial.setFloat("Wetness", rs.getWetness());
// Charakter-PBR: Emissive = customAmbient (IBL-Fallback ohne fertige LightProbe)
for (Material mat : characterPbrMaterials) {
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
@@ -808,7 +810,9 @@ public class WorldScene extends BaseAppState {
weather.setFogFilter(fogFilter);
weather.setViewDistanceFactor(graphicsSettings.viewDistance.fogFactor);
weather.setSkyControl(dayNight.getSkyControl());
weather.setDayTime(dayNight.getDayTime());
app.getStateManager().attach(weather);
app.getStateManager().attach(new de.blight.game.state.RainState());
} catch (Exception e) {
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());
}
@@ -828,11 +832,13 @@ public class WorldScene extends BaseAppState {
// Initiale Sonnenposition: von Kamera aus weit entfernt entgegen Lichtrichtung
Vector3f initSunPos = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
lightScatterFilter = new com.jme3.post.filters.LightScatteringFilter(initSunPos);
lightScatterFilter.setLightDensity(0.8f);
lightScatterFilter.setLightDensity(0.08f);
lightScatterFilter.setBlurStart(0.3f);
lightScatterFilter.setBlurWidth(0.4f);
lightScatterFilter.setNbSamples(150);
fpp.addFilter(lightScatterFilter);
WeatherState ws = app.getStateManager().getState(WeatherState.class);
if (ws != null) ws.setLightScatterFilter(lightScatterFilter);
}
if (pfx.toneMap) {

View File

@@ -0,0 +1,126 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.*;
import com.jme3.system.lwjgl.LwjglWindow;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWImage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* Ersetzt den Betriebssystem-Mauszeiger durch benutzerdefinierte GLFW-Cursor-Images.
* Tauscht automatisch zwischen Normalzustand und gedrücktem Zustand (LMB) aus.
*/
public class CursorState extends BaseAppState implements RawInputListener {
private static final Logger log = LoggerFactory.getLogger(CursorState.class);
private long windowHandle;
private long normalCursor;
private long pressedCursor;
@Override
protected void initialize(Application app) {
if (!(app.getContext() instanceof LwjglWindow)) {
log.warn("[Cursor] Kein LWJGL-Fenster Custom-Cursor nicht verfügbar.");
return;
}
windowHandle = ((LwjglWindow) app.getContext()).getWindowHandle();
if (windowHandle == 0L) return;
normalCursor = loadCursor(app, "Textures/internal/cursor/cursor.png");
pressedCursor = loadCursor(app, "Textures/internal/cursor/cursor_pressed.png");
if (normalCursor != 0L) {
GLFW.glfwSetCursor(windowHandle, normalCursor);
}
app.getInputManager().addRawInputListener(this);
log.info("[Cursor] Custom-Cursor geladen.");
}
@Override
protected void cleanup(Application app) {
app.getInputManager().removeRawInputListener(this);
if (windowHandle != 0L) {
GLFW.glfwSetCursor(windowHandle, 0L); // Standard-Cursor wiederherstellen
}
if (normalCursor != 0L) { GLFW.glfwDestroyCursor(normalCursor); normalCursor = 0L; }
if (pressedCursor != 0L) { GLFW.glfwDestroyCursor(pressedCursor); pressedCursor = 0L; }
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void onMouseButtonEvent(MouseButtonEvent evt) {
if (windowHandle == 0L) return;
if (evt.getButtonIndex() == 0) {
long c = evt.isPressed() ? pressedCursor : normalCursor;
if (c != 0L) GLFW.glfwSetCursor(windowHandle, c);
}
}
// ── RawInputListener-Stubs ────────────────────────────────────────────────
@Override public void beginInput() {}
@Override public void endInput() {}
@Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
@Override public void onKeyEvent(KeyInputEvent evt) {}
@Override public void onTouchEvent(TouchEvent evt) {}
// ── GLFW-Cursor aus Classpath-PNG laden ────────────────────────────────────
private static long loadCursor(Application app, String assetPath) {
try (InputStream is = app.getAssetManager()
.locateAsset(new com.jme3.asset.TextureKey(assetPath))
.openStream()) {
BufferedImage src = ImageIO.read(is);
if (src == null) throw new IllegalStateException("ImageIO konnte das Bild nicht lesen");
// Auf 32×32 skalieren GLFW rendert Cursor pixelgenau
int w = 32, h = 32;
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics2D g2 = img.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2.drawImage(src, 0, 0, w, h, null);
g2.dispose();
ByteBuffer pixels = ByteBuffer.allocateDirect(w * h * 4);
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int argb = img.getRGB(x, y);
pixels.put((byte) ((argb >> 16) & 0xFF)); // R
pixels.put((byte) ((argb >> 8) & 0xFF)); // G
pixels.put((byte) ( argb & 0xFF)); // B
pixels.put((byte) ((argb >> 24) & 0xFF)); // A
}
}
pixels.flip();
long cursor;
try (GLFWImage glfwImg = GLFWImage.malloc()) {
glfwImg.set(w, h, pixels);
cursor = GLFW.glfwCreateCursor(glfwImg, 0, 0);
}
if (cursor == 0L) throw new IllegalStateException("glfwCreateCursor schlug fehl");
return cursor;
} catch (Exception e) {
log.warn("[Cursor] Cursor nicht ladbar '{}': {}", assetPath, e.getMessage());
return 0L;
}
}
}

View File

@@ -6,13 +6,17 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.BulletAppState;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Sphere;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.shadow.EdgeFilteringMode;
import de.blight.common.time.DayTime;
import de.blight.common.time.TimeListener;
import jme3utilities.sky.CloudLayer;
import jme3utilities.sky.LunarPhase;
import jme3utilities.sky.SkyControl;
import jme3utilities.sky.StarsOption;
import org.slf4j.Logger;
@@ -42,6 +46,17 @@ public class DayNightState extends BaseAppState implements TimeListener {
private DirectionalLightShadowFilter shadowFilter;
private Node skyNode;
private SkyControl skyControl;
private Node cloudDomeNode;
private Material cloudMat;
private float cloudOffsetX = 0f;
private float cloudOffsetZ = 0f;
// ── Mond-Phase ────────────────────────────────────────────────────────────
/** 0 = Neumond, 0.5 = Vollmond, 1 = Neumond. Zyklus: 7 Spieltage = 6300 s Echtzeit. */
private float moonAge = 0.375f; // Start: zunehmend Gibbous
private LunarPhase currentMoonPhase = null; // zuletzt gesetzte Phase — vermeidet redundante Aufrufe
private static final float LUNAR_CYCLE = 6300f;
// ── Sonnenrichtungs-Drosselung ────────────────────────────────────────────
@@ -56,6 +71,10 @@ public class DayNightState extends BaseAppState implements TimeListener {
private float caveCheckTimer = 0f;
private ColorRGBA sunBaseColor = new ColorRGBA(1, 1, 1, 1);
private float shadowBaseIntensity = 0f;
/** Letzter berechneter Wert von sunOcclusionByCloud — für updateCaveLighting(). */
private float lastSunOcc = 0f;
/** 1.0 = klar, sinkt mit zunehmender Wolkendecke (min 0.20). */
private float cloudDimFactor = 1f;
private ColorRGBA customAmbient = new ColorRGBA();
/** true = Spiel-Beleuchtungswerte, false = Editor-Werte. Standardmäßig = withShadows. */
private boolean gameMode;
@@ -144,17 +163,31 @@ public class DayNightState extends BaseAppState implements TimeListener {
rootNode.attachChild(skyNode);
SkyControl sc = new SkyControl(app.getAssetManager(), app.getCamera(),
0.9f, StarsOption.Cube, true);
0f, StarsOption.Cube, true);
// SkyControl default: North=+X, but JME3 world: North=-Z, East=+X
sc.getSunAndStars().setAxes(new Vector3f(0f, 0f, -1f), Vector3f.UNIT_Y);
// Deckel-Kuppel leicht unter den Horizont verlängern → kein sichtbarer Saum beim Blick nach unten
sc.setTopVerticalAngle(FastMath.HALF_PI + 0.12f);
skyNode.addControl(sc);
CloudLayer clouds = sc.getCloudLayer(0);
clouds.setMotion(0.37f, 0f, 0.2f, 0.001f);
clouds.setTexture("Textures/skies/clouds/fbm.png", 0.3f);
clouds.setOpacity(0f);
// SkyControl-Wolkenlayer alle deaktivieren — CloudDome-Shader übernimmt
sc.setCloudiness(1.0f);
for (int li = 0; li < 6; li++) {
sc.getCloudLayer(li).setTexture("Textures/skies/clouds/clear.png", 1.0f);
}
// Prozedurale Wolkenkuppel (FBM-Shader, kein Sphere-UV-Drift)
Sphere cloudSphere = new Sphere(32, 64, 900f);
Geometry cloudGeo = new Geometry("cloudDome", cloudSphere);
cloudGeo.setQueueBucket(RenderQueue.Bucket.Sky);
cloudMat = new Material(app.getAssetManager(), "MatDefs/CloudDome.j3md");
cloudMat.setVector2("CloudOffset", new Vector2f(0f, 0f));
cloudMat.setFloat("CloudCover", 0f);
cloudMat.setColor("CloudColor", new ColorRGBA(1f, 1f, 1f, 1f));
cloudGeo.setMaterial(cloudMat);
cloudDomeNode = new Node("cloudDomeNode");
cloudDomeNode.attachChild(cloudGeo);
rootNode.attachChild(cloudDomeNode);
// Updater nur für Viewport-Hintergrundfarbe nutzen (Licht wird selbst gesteuert)
sc.getUpdater().addViewPort(this.app.getViewPort());
@@ -185,6 +218,11 @@ public class DayNightState extends BaseAppState implements TimeListener {
rootNode.detachChild(skyNode);
}
skyNode = null;
if (cloudDomeNode != null && cloudDomeNode.getParent() != null) {
rootNode.detachChild(cloudDomeNode);
}
cloudDomeNode = null;
cloudMat = null;
}
@Override protected void onEnable() {}
@@ -199,6 +237,60 @@ public class DayNightState extends BaseAppState implements TimeListener {
if (skyNode != null && skyNode.getParent() == null) {
rootNode.attachChild(skyNode);
}
if (cloudDomeNode != null && cloudDomeNode.getParent() == null) {
rootNode.attachChild(cloudDomeNode);
}
// Mond-Phase: 7 Spieltage pro Zyklus; 5 Phasen-Texturen über LunarPhase-Enum
moonAge += tpf / LUNAR_CYCLE;
if (moonAge >= 1f) moonAge -= 1f;
if (skyControl != null) {
LunarPhase phase = lunarPhaseFor(moonAge);
if (phase != currentMoonPhase) {
currentMoonPhase = phase;
skyControl.setPhase(phase);
}
}
// Wolkenkuppel: Kamera folgen, Wind-Scroll und Cover-Uniform setzen
WeatherState ws = app.getStateManager().getState(WeatherState.class);
float cloudCover = 0f;
if (cloudMat != null) {
Vector3f cam = app.getCamera().getLocation();
cloudDomeNode.setLocalTranslation(cam.x, cam.y, cam.z);
if (ws != null) {
Vector3f wind = ws.getWindDirection();
float scrollRate = ws.getWindSpeed() * 0.0002f * tpf;
cloudOffsetX += wind.x * scrollRate;
cloudOffsetZ += wind.z * scrollRate;
cloudCover = ws.getCloudCover();
cloudMat.setVector2("CloudOffset", new Vector2f(cloudOffsetX, cloudOffsetZ));
cloudMat.setFloat("CloudCover", cloudCover);
}
// Wolkenfarbe aus sunBaseColor; Minimum-Helligkeit für bewölkte Nächte
ColorRGBA cc = sunBaseColor.clone();
float lum = Math.max(cc.r, Math.max(cc.g, cc.b));
if (lum < 0.04f) {
cc.r = Math.max(cc.r, 0.04f);
cc.g = Math.max(cc.g, 0.04f);
cc.b = Math.max(cc.b, 0.05f);
}
cc.a = 1f;
cloudMat.setColor("CloudColor", cc);
}
// Sonnenlicht + Schatten: Okklusion durch CloudDome bestimmt beides
if (skyControl != null) {
Vector3f sunDir = skyControl.getSunAndStars().sunDirection(null);
lastSunOcc = sunOcclusionByCloud(sunDir, cloudCover);
// Direktlicht: bis 92% gedimmt wenn Sonne verdeckt; min 8% bleibt (diffuses Streulicht)
float sunDirect = FastMath.clamp(1f - lastSunOcc * 0.92f, 0.08f, 1f);
sun.setColor(sunBaseColor.mult(sunDirect * (1f - caveFactor)));
if (shadowFilter != null) {
shadowFilter.setShadowIntensity(shadowBaseIntensity * (1f - lastSunOcc) * (1f - caveFactor));
}
}
updateCaveLighting(tpf, app.getCamera().getLocation());
}
@@ -224,14 +316,69 @@ public class DayNightState extends BaseAppState implements TimeListener {
}
if (caveFactor != prev) {
float scale = 1f - caveFactor;
sun.setColor(sunBaseColor.mult(scale));
if (shadowFilter != null) {
shadowFilter.setShadowIntensity(shadowBaseIntensity * scale);
}
float sunDirect = FastMath.clamp(1f - lastSunOcc * 0.92f, 0.08f, 1f);
sun.setColor(sunBaseColor.mult(sunDirect * (1f - caveFactor)));
}
}
// ── Mond-Phasen-Mapping ───────────────────────────────────────────────────
/**
* Bildet moonAge (0=Neumond, 0.5=Vollmond) auf die fünf LunarPhase-Texturen ab.
* setPhase(LunarPhase) setzt gleichzeitig den Längenunterschied zur Sonne,
* sodass der Mond sich korrekt relativ zur Sonne positioniert.
*/
private static LunarPhase lunarPhaseFor(float age) {
if (age < 0.20f) return LunarPhase.WAXING_CRESCENT;
if (age < 0.40f) return LunarPhase.WAXING_GIBBOUS;
if (age < 0.60f) return LunarPhase.FULL;
if (age < 0.80f) return LunarPhase.WANING_GIBBOUS;
return LunarPhase.WANING_CRESCENT;
}
// ── Cloud-Okklusion (CPU-Replikation des CloudDome-Shaders) ──────────────
/**
* Gibt zurück, wie stark die Sonne durch den CloudDome verdeckt ist (0=frei, 1=verdeckt).
* Gleiche FBM-Formel wie CloudDome.frag, ausgewertet für die Sonnenrichtung.
*/
private float sunOcclusionByCloud(Vector3f sunDir, float cloudCover) {
if (sunDir.y <= 0.02f) return 1f; // Sonne unter Horizont → keine Schatten sowieso
float denom = sunDir.y + 1.001f;
float u = sunDir.x / denom * 3.5f + cloudOffsetX;
float v = sunDir.z / denom * 3.5f + cloudOffsetZ;
float f = fbmCpu(u, v);
float threshold = 0.72f - 0.5f * cloudCover; // mix(0.72, 0.22, cloudCover)
float t = FastMath.clamp((f - (threshold - 0.09f)) / 0.18f, 0f, 1f);
return t * t * (3f - 2f * t); // smoothstep
}
private static float fbmCpu(float px, float py) {
float v = 0f, a = 0.5f;
for (int i = 0; i < 6; i++) {
v += a * vnoiseCpu(px, py);
px = px * 2.17f + 0.631f;
py = py * 2.17f + 1.137f;
a *= 0.5f;
}
return v;
}
private static float vnoiseCpu(float px, float py) {
float ix = (float) Math.floor(px), iy = (float) Math.floor(py);
float fx = px - ix, fy = py - iy;
fx = fx * fx * (3f - 2f * fx);
fy = fy * fy * (3f - 2f * fy);
float a = hashCpu(ix, iy) + fx * (hashCpu(ix + 1f, iy) - hashCpu(ix, iy));
float b = hashCpu(ix, iy + 1f) + fx * (hashCpu(ix + 1f, iy + 1f) - hashCpu(ix, iy + 1f));
return a + fy * (b - a);
}
private static float hashCpu(float x, float y) {
double s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;
return (float) (s - Math.floor(s));
}
// ── Zeit-Callback ─────────────────────────────────────────────────────────
@Override
@@ -251,12 +398,17 @@ public class DayNightState extends BaseAppState implements TimeListener {
ColorRGBA custNight = gameMode ? LightingConfig.CUSTOM_AMB_NIGHT : LightingConfig.EDITOR_CUSTOM_AMB_NIGHT;
float sunIntensity = gameMode ? LightingConfig.SUN_INTENSITY : LightingConfig.EDITOR_SUN_INTENSITY;
// ── Wolkendecke dämpft Sonnenlicht und Schatten ──────────────────────────
WeatherState ws = app.getStateManager().getState(WeatherState.class);
float cloudCover = ws != null ? ws.getCloudCover() : 0f;
cloudDimFactor = Math.max(0.20f, 1f - cloudCover * 0.80f);
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) — jedes Frame ──
float dawnFactor = 1f - FastMath.clamp(elevation * LightingConfig.SUN_DAWN_SLOPE, 0f, 1f);
ColorRGBA sunTint = LightingConfig.SUN_COLOR_DAWN.clone()
.interpolateLocal(LightingConfig.SUN_COLOR_DAY, 1f - dawnFactor);
sunBaseColor = sunTint.mult(elevC * sunIntensity);
sun.setColor(sunBaseColor.mult(1f - caveFactor));
sunBaseColor = sunTint.mult(elevC * sunIntensity * cloudDimFactor);
// sun.setColor() wird jedes Frame in update() mit sunOcc-Faktor gesetzt
// ── Ambient: Nacht (blau) → Tag (Sonnenfarbe-tinted) — jedes Frame ──
// Tages-Ambient bekommt den Farbton der Sonne: orange bei Dämmerung,
@@ -268,16 +420,15 @@ public class DayNightState extends BaseAppState implements TimeListener {
ambient.setColor(ambNight.clone().interpolateLocal(ambDayTinted, ambFactor));
customAmbient.set(custNight.clone().interpolateLocal(custDayTinted, ambFactor));
// Basis-Schattenintensität: nur von Sonnenhöhe abhängig.
// Wolken-Okklusion wird jedes Frame separat in update() berechnet.
shadowBaseIntensity = FastMath.clamp(
elevation * LightingConfig.SHADOW_SLOPE, 0f, LightingConfig.SHADOW_MAX);
// Lichtrichtung und Schatten nur 4×/Sek aktualisieren → kein Shadow-Map-Flackern
// Lichtrichtung nur alle 2 s aktualisieren → kein Shadow-Map-Flackern
if (sunDirTimer >= SUN_DIR_INTERVAL) {
sunDirTimer = 0f;
sun.setDirection(toSun.negateLocal());
if (shadowFilter != null) {
shadowFilter.setShadowIntensity(shadowBaseIntensity * (1f - caveFactor));
}
}
}
}

View File

@@ -112,6 +112,28 @@ public class GrassState extends BaseAppState
}
}
}
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
if (ws != null) {
Vector3f wd3 = ws.getWindDirection();
Vector2f wDir = new Vector2f(wd3.x, wd3.z);
float speed = Math.max(0.05f, ws.getWindSpeed() * 0.04f);
float strength = FastMath.clamp(ws.getWindSpeed() * 0.009f, 0.01f, 0.45f);
for (Material mat : slotMaterials.values()) {
mat.setVector2("WindDir", wDir);
mat.setFloat("WindSpeed", speed);
mat.setFloat("WindStrength", strength);
}
}
RainState rs = getApplication().getStateManager().getState(RainState.class);
if (rs != null) {
float w = rs.getWetness();
for (Material mat : slotMaterials.values()) {
if (mat.getMaterialDef().getMaterialParam("Wetness") != null)
mat.setFloat("Wetness", w);
}
}
}
// ── Material ──────────────────────────────────────────────────────────────

View File

@@ -193,6 +193,20 @@ public class GrassVertexRenderState extends BaseAppState
sm.setFloat("WindStrength", strength);
}
}
RainState rs = getApplication().getStateManager().getState(RainState.class);
if (rs != null) {
float w = rs.getWetness();
setWetness(material, w);
setWetness(seedStalkMaterial, w);
for (Material sm : seedMaterials) setWetness(sm, w);
}
}
private static void setWetness(Material mat, float w) {
if (mat != null && mat.getMaterialDef().getMaterialParam("Wetness") != null) {
mat.setFloat("Wetness", w);
}
}
// ── Material ──────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,310 @@
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.audio.AudioData;
import com.jme3.audio.AudioNode;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.effect.shapes.EmitterBoxShape;
import com.jme3.light.AmbientLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.texture.Image;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;
/**
* Regen-Partikel, Nass-Effekt, Gewitter-Blitz und Donner-Audio.
* Wetness (0..1) wird von anderen States abgefragt und als Shader-Uniform gesetzt.
*/
public class RainState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(RainState.class);
// ── Wetness ───────────────────────────────────────────────────────────────
private static final float WET_RATE_UP = 0.020f; // pro Sekunde
private static final float WET_RATE_DOWN = 0.005f; // langsames Abtrocknen
private float wetness = 0f;
// ── Regen-Partikel ────────────────────────────────────────────────────────
private static final float EMITTER_HEIGHT = 26f;
private static final float EMITTER_RADIUS = 45f;
@SuppressWarnings("deprecation")
private ParticleEmitter rainEmitter;
private Node rainNode;
// ── Audio ─────────────────────────────────────────────────────────────────
private AudioNode lightRainAudio;
private AudioNode heavyRainAudio;
private final AudioNode[] thunderAudio = new AudioNode[3];
private float lightRainVol = 0f;
private float heavyRainVol = 0f;
// ── Blitz / Donner ────────────────────────────────────────────────────────
private AmbientLight lightningLight;
private float nextLightningTimer = 18f;
private float flashTimer = 0f;
private static final float FLASH_DURATION = 0.13f;
private boolean thunderPending = false;
private float thunderTimer = 0f;
private int thunderIdx = 0;
// ── Persistenz ────────────────────────────────────────────────────────────
private float saveTimer = 60f;
// ─────────────────────────────────────────────────────────────────────────
/** Nässe-Faktor 0..1 — wird von Terrain/Objekt-States als Shader-Uniform gesetzt. */
public float getWetness() { return wetness; }
@Override
@SuppressWarnings("deprecation")
protected void initialize(Application app) {
SimpleApplication sa = (SimpleApplication) app;
Node root = sa.getRootNode();
AssetManager assets = app.getAssetManager();
// Blitz-Licht (startet bei Intensität 0)
lightningLight = new AmbientLight(new ColorRGBA(0, 0, 0, 1));
root.addLight(lightningLight);
// Regen-Partikel
rainEmitter = new ParticleEmitter("rain", ParticleMesh.Type.Triangle, 5000);
rainEmitter.setShape(new EmitterBoxShape(
new Vector3f(-EMITTER_RADIUS, -1f, -EMITTER_RADIUS),
new Vector3f( EMITTER_RADIUS, 1f, EMITTER_RADIUS)));
rainEmitter.setGravity(0, 0, 0);
rainEmitter.setParticlesPerSec(0);
rainEmitter.setLowLife(0.9f);
rainEmitter.setHighLife(1.2f);
rainEmitter.setStartSize(0.15f);
rainEmitter.setEndSize(0.06f);
rainEmitter.setStartColor(new ColorRGBA(0.82f, 0.90f, 1.00f, 0.80f));
rainEmitter.setEndColor( new ColorRGBA(0.82f, 0.90f, 1.00f, 0.00f));
rainEmitter.setInitialVelocity(new Vector3f(0f, -26f, 0f));
rainEmitter.setVelocityVariation(0.06f);
com.jme3.material.Material pm =
new com.jme3.material.Material(assets, "Common/MatDefs/Misc/Particle.j3md");
pm.setTexture("Texture", createRainTexture());
pm.getAdditionalRenderState()
.setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
pm.getAdditionalRenderState().setDepthWrite(false);
rainEmitter.setMaterial(pm);
rainNode = new Node("rainNode");
rainNode.attachChild(rainEmitter);
root.attachChild(rainNode);
// Regen-Audio (gestreamt, da lange Schleifen)
lightRainAudio = streamAudio(assets, "audio/ambient/weather/leichter_regen.ogg");
heavyRainAudio = streamAudio(assets, "audio/ambient/weather/starker_regen.ogg");
root.attachChild(lightRainAudio);
root.attachChild(heavyRainAudio);
lightRainAudio.play();
heavyRainAudio.play();
// Donner (gepuffert, kurze Clips)
for (int i = 0; i < 3; i++) {
thunderAudio[i] = bufferAudio(assets, "audio/ambient/weather/donner" + (i + 1) + ".ogg");
root.attachChild(thunderAudio[i]);
}
loadWetness();
log.info("[Rain] initialisiert.");
}
@Override
@SuppressWarnings("deprecation")
public void update(float tpf) {
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
if (ws == null) return;
float intensity = ws.getRainIntensity();
WeatherState.Weather weather = ws.getActiveWeather();
boolean stormy = weather == WeatherState.Weather.THUNDERSTORM;
saveTimer -= tpf;
if (saveTimer <= 0f) { saveTimer = 60f; saveWetness(); }
// ── Wetness ───────────────────────────────────────────────────────────
float wetnessTarget = intensity > 0.01f ? FastMath.clamp(intensity + 0.25f, 0f, 1f) : 0f;
if (wetness < wetnessTarget) {
wetness = Math.min(wetnessTarget, wetness + WET_RATE_UP * tpf);
} else {
wetness = Math.max(wetnessTarget, wetness - WET_RATE_DOWN * tpf);
}
// ── Partikel ──────────────────────────────────────────────────────────
Vector3f cam = getApplication().getCamera().getLocation();
rainNode.setLocalTranslation(cam.x, cam.y + EMITTER_HEIGHT, cam.z);
Vector3f wind = ws.getWindDirection();
float ws2 = ws.getWindSpeed() * 0.07f;
rainEmitter.getParticleInfluencer()
.setInitialVelocity(new Vector3f(wind.x * ws2, -26f, wind.z * ws2));
rainEmitter.setParticlesPerSec(intensity * 2500f);
// ── Audio ─────────────────────────────────────────────────────────────
float tgtLight = intensity > 0.01f ? FastMath.clamp(1f - intensity * 1.2f, 0f, 0.225f) : 0f;
float tgtHeavy = FastMath.clamp((intensity - 0.20f) * 1.35f, 0f, 0.25f);
AudioSettingsState audioState = getApplication().getStateManager()
.getState(AudioSettingsState.class);
float ambVol = audioState != null ? audioState.effectiveAmbient() : 1f;
float alpha = FastMath.clamp(tpf * 2.5f, 0f, 1f);
lightRainVol += (tgtLight - lightRainVol) * alpha;
heavyRainVol += (tgtHeavy - heavyRainVol) * alpha;
lightRainAudio.setVolume(lightRainVol * ambVol);
heavyRainAudio.setVolume(heavyRainVol * ambVol);
// ── Blitz-Flash ───────────────────────────────────────────────────────
if (flashTimer > 0f) {
flashTimer = Math.max(0f, flashTimer - tpf);
float phase = 1f - flashTimer / FLASH_DURATION;
float fi = (float) Math.sin(phase * Math.PI);
lightningLight.setColor(new ColorRGBA(fi, fi * 0.95f, fi * 0.85f, 1f));
} else {
lightningLight.setColor(new ColorRGBA(0f, 0f, 0f, 1f));
}
// ── Donner-Verzögerung ────────────────────────────────────────────────
if (thunderPending) {
thunderTimer -= tpf;
if (thunderTimer <= 0f) {
thunderPending = false;
float vol = FastMath.clamp(1f - (2.5f + thunderTimer) / 2.5f, 0.15f, 1f);
thunderAudio[thunderIdx].setVolume(vol);
thunderAudio[thunderIdx].playInstance();
}
}
// ── Nächster Blitz ────────────────────────────────────────────────────
nextLightningTimer -= tpf;
if (nextLightningTimer <= 0f) {
if (stormy) {
// Gewitter: Blitz + Donner alle 8-28s
triggerLightning(true);
nextLightningTimer = 8f + FastMath.nextRandomFloat() * 20f;
} else if (weather == WeatherState.Weather.RAIN_HEAVY) {
// Starker Regen: ferner Donner ohne Blitz alle 40-80s
triggerLightning(false);
nextLightningTimer = 40f + FastMath.nextRandomFloat() * 40f;
} else {
nextLightningTimer = 30f;
}
}
}
private void triggerLightning(boolean withFlash) {
if (withFlash) flashTimer = FLASH_DURATION;
thunderIdx = (int)(FastMath.nextRandomFloat() * 3);
float delay = FastMath.nextRandomFloat() * 2.5f + 0.3f;
thunderPending = true;
thunderTimer = delay;
}
@Override
protected void onDisable() {
if (rainEmitter != null) rainEmitter.setParticlesPerSec(0);
if (lightRainAudio != null) lightRainAudio.setVolume(0);
if (heavyRainAudio != null) heavyRainAudio.setVolume(0);
}
@Override protected void onEnable() {}
@Override
protected void cleanup(Application app) {
saveWetness();
Node root = ((SimpleApplication) app).getRootNode();
if (lightningLight != null) root.removeLight(lightningLight);
if (rainNode != null) root.detachChild(rainNode);
if (lightRainAudio != null) { lightRainAudio.stop(); root.detachChild(lightRainAudio); }
if (heavyRainAudio != null) { heavyRainAudio.stop(); root.detachChild(heavyRainAudio); }
for (AudioNode tn : thunderAudio) { if (tn != null) root.detachChild(tn); }
}
// ── Persistenz ────────────────────────────────────────────────────────────
private static File wetFile() {
File dir = new File(System.getProperty("user.home"), ".blight");
dir.mkdirs();
return new File(dir, "rain.properties");
}
private void saveWetness() {
Properties p = new Properties();
p.setProperty("wetness", String.valueOf(wetness));
try (FileWriter fw = new FileWriter(wetFile())) {
p.store(fw, null);
} catch (Exception e) {
log.warn("[Rain] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
private void loadWetness() {
File f = wetFile();
if (!f.exists()) return;
Properties p = new Properties();
try (FileReader fr = new FileReader(f)) {
p.load(fr);
wetness = Float.parseFloat(p.getProperty("wetness", "0"));
} catch (Exception e) {
log.warn("[Rain] Laden fehlgeschlagen: {}", e.getMessage());
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Texture2D createRainTexture() {
int w = 4, h = 32;
ByteBuffer buf = ByteBuffer.allocateDirect(w * h * 4);
for (int y = 0; y < h; y++) {
float fy = (float) y / (h - 1);
// Oben ausgeblendet (Tropfen erscheint), unten voll sichtbar → Strich-Silhouette
float ay = FastMath.clamp(fy * 5f, 0f, 1f) * FastMath.clamp((1f - fy) * 2f, 0f, 1f);
for (int x = 0; x < w; x++) {
float fx = Math.abs((x - (w - 1) * 0.5f) / ((w - 1) * 0.5f));
float ax = FastMath.clamp(1f - fx * 1.5f, 0f, 1f);
float alpha = ay * ax;
buf.put((byte) 210);
buf.put((byte) 228);
buf.put((byte) 255);
buf.put((byte) (int) (alpha * 230));
}
}
buf.flip();
return new Texture2D(new Image(Image.Format.RGBA8, w, h, buf, ColorSpace.Linear));
}
private static AudioNode streamAudio(AssetManager assets, String path) {
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream);
n.setPositional(false);
n.setLooping(true);
n.setVolume(0f);
return n;
}
private static AudioNode bufferAudio(AssetManager assets, String path) {
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Buffer);
n.setPositional(false);
n.setLooping(false);
n.setVolume(1f);
return n;
}
}

View File

@@ -30,6 +30,7 @@ public class StoneWorldState extends BaseAppState {
private BulletAppState bulletAppState;
private TerrainChunkState terrainChunkState;
private Node stoneRoot;
private final List<Material> stoneMaterials = new ArrayList<>();
@Override
protected void initialize(Application app) {
@@ -56,6 +57,7 @@ public class StoneWorldState extends BaseAppState {
return;
}
stoneMaterials.clear();
Material[] slotMat = buildMaterials(data.slotPaths());
Material defMat = buildDefaultMat();
@@ -86,12 +88,27 @@ public class StoneWorldState extends BaseAppState {
}
}
stoneMaterials.add(mat);
stoneRoot.attachChild(geo);
count++;
}
log.info("[StoneWorld] {} Steine geladen.", count);
}
@Override
public void update(float tpf) {
if (stoneMaterials.isEmpty()) return;
RainState rs = getApplication().getStateManager().getState(RainState.class);
if (rs == null) return;
float w = rs.getWetness();
float roughness = 0.85f * (1f - w * 0.80f);
float metallic = w * 0.10f;
for (Material mat : stoneMaterials) {
mat.setFloat("Roughness", roughness);
mat.setFloat("Metallic", metallic);
}
}
@Override
protected void cleanup(Application app) {
this.app.getRootNode().detachChild(stoneRoot);

View File

@@ -219,6 +219,7 @@ public class TerrainChunkState extends BaseAppState {
}
}
// Loop 1: Meshes für schmutzige Chunks + Nachbarn neu aufbauen
for (int cz = 0; cz < N; cz++) {
for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
@@ -229,12 +230,6 @@ public class TerrainChunkState extends BaseAppState {
rebuildChunkMesh(cx, cz, newLod, targetLod);
// Physik: nur für nahe Chunks halten
int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz);
boolean wantsPhysics = dist <= ChunkTerrainIO.PHYSICS_RANGE;
if (wantsPhysics && physics[ci] == null) addPhysics(ci);
if (!wantsPhysics && physics[ci] != null) removePhysics(ci);
// Listener benachrichtigen
if (oldLod < 0) {
notifyVisible(cx, cz, newLod);
@@ -243,6 +238,18 @@ public class TerrainChunkState extends BaseAppState {
}
}
}
// Loop 2: Physik für alle Chunks aktualisieren nach dem Mesh-Rebuild,
// damit chunkNodes bereits existieren wenn addPhysics aufgerufen wird.
for (int cz = 0; cz < N; cz++) {
for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz);
boolean wantsPhysics = dist <= ChunkTerrainIO.PHYSICS_RANGE;
if (wantsPhysics && physics[ci] == null) addPhysics(ci);
if (!wantsPhysics && physics[ci] != null) removePhysics(ci);
}
}
}
// ── Öffentliche API ───────────────────────────────────────────────────────

View File

@@ -6,136 +6,324 @@ import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import de.blight.common.time.DayTime;
import de.blight.game.post.BlightFogFilter;
import com.jme3.water.WaterFilter;
import jme3utilities.sky.SkyControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;
public class WeatherState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WeatherState.class);
public enum Weather { SUNNY, CLOUDY, OVERCAST, STORM, FOG }
/**
* Wetterübergänge folgen meteorologischer Logik: Regen eskaliert stufenweise,
* ein Gewitter setzt immer starken Regen voraus. CALIMA (Sahara-Staub) ist
* kanaren-typisch.
*
* Ordinal-Reihenfolge darf nicht verändert werden — RAIN_INTENSITY und alle
* anderen Arrays sind darauf indiziert.
*/
public enum Weather {
SUNNY, // 0 — strahlend blauer Himmel
PARTLY_CLOUDY, // 1 — einzelne Cumulus-Wolken
CLOUDY, // 2 — bedeckt, aber trocken
OVERCAST, // 3 — dunkle Wolkendecke
FOG, // 4 — Passatwind-Nebel (Bruma)
DRIZZLE, // 5 — leichter Nieselregen
RAIN_LIGHT, // 6 — leichter Regen
RAIN_MEDIUM, // 7 — mäßiger Regen
RAIN_HEAVY, // 8 — starker Regen
THUNDERSTORM, // 9 — Gewitter mit Blitz & Donner
CALIMA, // 10 — Sahara-Staub, gelblicher Dunst
WIND_STORM // 11 — Sturm ohne Regen (Tramontana)
}
// ── Per-weather targets (Reihenfolge: SUNNY, CLOUDY, OVERCAST, STORM, FOG)
// ── Per-Zustand-Zielwerte (Reihenfolge = Weather.ordinal()) ─────────────
private static final float[] FOG_DENSITY = { 0.40f, 0.55f, 0.75f, 0.90f, 0.88f };
private static final float[] FOG_DISTANCE = { 600f, 350f, 140f, 50f, 80f };
private static final float[] WIND_SPEED = { 4f, 14f, 26f, 55f, 3f };
private static final float[] WAVE_SPEED = { 0.5f, 1.0f, 1.5f, 3.2f, 0.4f };
private static final float[] WAVE_AMP = { 0.3f, 0.5f, 0.8f, 1.8f, 0.2f };
private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f, 0.009f};
private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f, 0.12f };
private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f, 0.0f };
private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f, 0.95f };
private static final float[] FOG_DENSITY = {
0.10f, 0.18f, 0.35f, 0.65f, 0.88f,
0.55f, 0.50f, 0.72f, 0.85f, 0.92f,
0.70f, 0.22f
};
private static final float[] FOG_DISTANCE = {
800f, 600f, 400f, 180f, 80f,
220f, 280f, 130f, 70f, 50f,
120f, 500f
};
private static final float[] WIND_SPEED = {
3f, 8f, 16f, 22f, 2f,
10f, 14f, 28f, 42f, 62f,
20f, 72f
};
private static final float[] WAVE_SPEED = {
0.4f, 0.6f, 0.9f, 1.3f, 0.3f,
0.7f, 1.0f, 1.8f, 2.5f, 3.5f,
0.8f, 3.8f
};
private static final float[] WAVE_AMP = {
0.20f, 0.30f, 0.50f, 0.75f, 0.15f,
0.40f, 0.50f, 1.00f, 1.40f, 2.00f,
0.35f, 2.20f
};
private static final float[] WAVE_SCALE = {
0.009f, 0.008f, 0.007f, 0.006f, 0.010f,
0.007f, 0.007f, 0.006f, 0.005f, 0.005f,
0.008f, 0.005f
};
private static final float[] WATER_TRANS = {
0.18f, 0.14f, 0.10f, 0.07f, 0.13f,
0.09f, 0.09f, 0.04f, 0.02f, 0.01f,
0.12f, 0.06f
};
private static final float[] FOAM_INTENSITY = {
0.00f, 0.05f, 0.20f, 0.40f, 0.00f,
0.15f, 0.20f, 0.55f, 0.75f, 0.95f,
0.10f, 0.80f
};
private static final float[] CLOUD_OPACITY = {
0.00f, 0.30f, 0.65f, 0.90f, 0.95f,
0.75f, 0.85f, 0.95f, 1.00f, 1.00f,
0.60f, 0.50f
};
/** Opazität der gleichmäßigen Überdeckungs-Wolkenschicht (Layer 1). */
private static final float[] OVERCAST_OPACITY = {
0.00f, 0.08f, 0.35f, 0.60f, 0.50f,
0.42f, 0.52f, 0.68f, 0.82f, 0.90f,
0.12f, 0.28f
};
/** 0 = kein Regen, 1 = Gewitter. */
private static final float[] RAIN_INTENSITY = {
0f, 0f, 0f, 0f, 0f,
0.12f, 0.33f, 0.67f, 0.88f, 1.00f,
0f, 0f
};
/** Minimale Dauer des Zustands in Sekunden. */
private static final float[] DURATION_MIN = {
180f, 90f, 60f, 60f, 120f,
45f, 60f, 45f, 30f, 20f,
180f, 90f
};
/** Maximale Dauer des Zustands in Sekunden. */
private static final float[] DURATION_MAX = {
600f, 300f, 240f, 180f, 420f,
150f, 210f, 150f, 90f, 60f,
480f, 270f
};
private static final ColorRGBA[] FOG_COLOR = {
new ColorRGBA(0.75f, 0.80f, 0.88f, 1f),
new ColorRGBA(0.62f, 0.65f, 0.70f, 1f),
new ColorRGBA(0.42f, 0.43f, 0.46f, 1f),
new ColorRGBA(0.18f, 0.19f, 0.21f, 1f),
new ColorRGBA(0.70f, 0.72f, 0.75f, 1f),
new ColorRGBA(0.78f, 0.85f, 0.95f, 1f), // SUNNY
new ColorRGBA(0.75f, 0.80f, 0.90f, 1f), // PARTLY_CLOUDY
new ColorRGBA(0.62f, 0.65f, 0.72f, 1f), // CLOUDY
new ColorRGBA(0.42f, 0.44f, 0.48f, 1f), // OVERCAST
new ColorRGBA(0.70f, 0.72f, 0.75f, 1f), // FOG
new ColorRGBA(0.50f, 0.52f, 0.58f, 1f), // DRIZZLE
new ColorRGBA(0.45f, 0.48f, 0.55f, 1f), // RAIN_LIGHT
new ColorRGBA(0.28f, 0.30f, 0.35f, 1f), // RAIN_MEDIUM
new ColorRGBA(0.18f, 0.19f, 0.22f, 1f), // RAIN_HEAVY
new ColorRGBA(0.12f, 0.13f, 0.16f, 1f), // THUNDERSTORM
new ColorRGBA(0.82f, 0.74f, 0.52f, 1f), // CALIMA — Sahara-Gelbton
new ColorRGBA(0.60f, 0.62f, 0.68f, 1f), // WIND_STORM
};
private static final ColorRGBA[] WATER_COLOR = {
new ColorRGBA(0.05f, 0.25f, 0.55f, 1f),
new ColorRGBA(0.05f, 0.22f, 0.50f, 1f),
new ColorRGBA(0.04f, 0.18f, 0.42f, 1f),
new ColorRGBA(0.03f, 0.12f, 0.28f, 1f),
new ColorRGBA(0.02f, 0.06f, 0.14f, 1f),
new ColorRGBA(0.04f, 0.20f, 0.45f, 1f),
new ColorRGBA(0.03f, 0.15f, 0.36f, 1f),
new ColorRGBA(0.03f, 0.15f, 0.38f, 1f),
new ColorRGBA(0.02f, 0.09f, 0.22f, 1f),
new ColorRGBA(0.01f, 0.05f, 0.14f, 1f),
new ColorRGBA(0.01f, 0.03f, 0.09f, 1f),
new ColorRGBA(0.06f, 0.22f, 0.42f, 1f), // CALIMA — leicht trüb
new ColorRGBA(0.02f, 0.10f, 0.30f, 1f),
};
private static final ColorRGBA[] DEEP_WATER_COLOR = {
new ColorRGBA(0.02f, 0.12f, 0.30f, 1f),
new ColorRGBA(0.02f, 0.10f, 0.26f, 1f),
new ColorRGBA(0.01f, 0.08f, 0.20f, 1f),
new ColorRGBA(0.01f, 0.04f, 0.12f, 1f),
new ColorRGBA(0.00f, 0.02f, 0.06f, 1f),
new ColorRGBA(0.01f, 0.09f, 0.23f, 1f),
new ColorRGBA(0.01f, 0.07f, 0.18f, 1f),
new ColorRGBA(0.01f, 0.06f, 0.18f, 1f),
new ColorRGBA(0.00f, 0.03f, 0.10f, 1f),
new ColorRGBA(0.00f, 0.02f, 0.06f, 1f),
new ColorRGBA(0.00f, 0.01f, 0.04f, 1f),
new ColorRGBA(0.02f, 0.09f, 0.20f, 1f),
new ColorRGBA(0.01f, 0.04f, 0.14f, 1f),
};
// ── State ────────────────────────────────────────────────────────────────
/**
* Übergangsmatrix — Wiederholungen erhöhen die Wahrscheinlichkeit.
* Regen darf nur stufenweise eskalieren; direkter Sprung von SUNNY zu
* THUNDERSTORM ist nicht möglich.
*/
private static final Weather[][] TRANSITIONS = {
// SUNNY: fast immer erst PARTLY_CLOUDY, selten Calima oder Sturm
{ Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY,
Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY,
Weather.CALIMA, Weather.CALIMA, Weather.WIND_STORM },
// PARTLY_CLOUDY: häufig Besserung, manchmal Eintrübung
{ Weather.SUNNY, Weather.SUNNY, Weather.SUNNY,
Weather.CLOUDY, Weather.CLOUDY, Weather.WIND_STORM },
// CLOUDY: kann zu Niesel, Überbedeckung oder Besserung wechseln
{ Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY,
Weather.OVERCAST, Weather.OVERCAST, Weather.DRIZZLE, Weather.WIND_STORM },
// OVERCAST: Nebel, Niesel oder leichter Regen, selten Besserung
{ Weather.CLOUDY, Weather.CLOUDY,
Weather.FOG, Weather.DRIZZLE, Weather.DRIZZLE, Weather.RAIN_LIGHT },
// FOG: löst sich langsam auf oder geht in Niesel über
{ Weather.OVERCAST, Weather.OVERCAST, Weather.CLOUDY, Weather.DRIZZLE },
// DRIZZLE: kann intensivieren oder aufhören
{ Weather.OVERCAST, Weather.OVERCAST, Weather.RAIN_LIGHT, Weather.CLOUDY },
// RAIN_LIGHT: eskaliert oder beruhigt sich zu Niesel
{ Weather.DRIZZLE, Weather.DRIZZLE,
Weather.RAIN_MEDIUM, Weather.OVERCAST, Weather.OVERCAST },
// RAIN_MEDIUM: eskaliert zu RAIN_HEAVY oder lässt nach
{ Weather.RAIN_LIGHT, Weather.RAIN_LIGHT, Weather.RAIN_HEAVY, Weather.OVERCAST },
// RAIN_HEAVY: kann zu Gewitter eskalieren oder nachlassen
{ Weather.RAIN_MEDIUM, Weather.RAIN_MEDIUM, Weather.THUNDERSTORM, Weather.OVERCAST },
// THUNDERSTORM: beruhigt sich immer stufenweise
{ Weather.RAIN_HEAVY, Weather.RAIN_HEAVY, Weather.RAIN_MEDIUM, Weather.OVERCAST },
// CALIMA: meist Aufklärung, selten Überbedeckung
{ Weather.SUNNY, Weather.SUNNY, Weather.PARTLY_CLOUDY, Weather.OVERCAST },
// WIND_STORM: klingt über Bewölkung oder Überbedeckung ab
{ Weather.CLOUDY, Weather.PARTLY_CLOUDY, Weather.PARTLY_CLOUDY, Weather.OVERCAST },
};
// ── Laufzeit-State ────────────────────────────────────────────────────────
private Weather active = Weather.SUNNY;
private float changeTimer = 120f;
private float changeTimer = 180f;
private float saveTimer = 60f;
private float fogDensity = 0.40f; // Startwert = SUNNY-Ziel, kein langsames Fade-in
private float fogDistance = 600f;
private float windSpeed = 4f;
private float waveSpeed = 0.5f;
private float waveAmp = 0.3f;
private float waveScale = 0.008f;
private float waterTrans = 0.15f;
private float foamIntensity = 0f;
private float cloudOpacity = 0f;
private float windAngle = 0f;
private float windAngleTgt = 0.4f;
private final ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f);
private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f);
private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f);
private float fogDensity = FOG_DENSITY[0];
private float fogDistance = FOG_DISTANCE[0];
private float windSpeed = WIND_SPEED[0];
private float waveSpeed = WAVE_SPEED[0];
private float waveAmp = WAVE_AMP[0];
private float waveScale = WAVE_SCALE[0];
private float waterTrans = WATER_TRANS[0];
private float foamIntensity = FOAM_INTENSITY[0];
private float cloudOpacity = CLOUD_OPACITY[0];
private float cloudOvercastOpacity = OVERCAST_OPACITY[0];
private float cloudOffsetU = 0f;
private float cloudOffsetV = 0f;
private float windAngle = 0f;
private float windAngleTgt = 0.4f;
// ── Sichtweiten-Faktor (aus Grafikeinstellungen) ──────────────────────────
private final ColorRGBA fogColor = FOG_COLOR[0].clone();
private final ColorRGBA waterColor = WATER_COLOR[0].clone();
private final ColorRGBA deepWaterColor = DEEP_WATER_COLOR[0].clone();
private float viewDistanceFactor = 1.0f;
public void setViewDistanceFactor(float f) { this.viewDistanceFactor = f; }
// ── Externe Referenzen ────────────────────────────────────────────────────
private BlightFogFilter fogFilter;
private WaterFilter waterFilter;
private SkyControl skyControl;
private com.jme3.post.filters.LightScatteringFilter lightScatterFilter;
private DayTime dayTime;
public void setViewDistanceFactor(float f) { this.viewDistanceFactor = f; }
public void setFogFilter(BlightFogFilter f) { this.fogFilter = f; }
public void setWaterFilter(WaterFilter f) { this.waterFilter = f; }
public void setSkyControl(SkyControl sc) { this.skyControl = sc; }
public void setWaterFilter(WaterFilter f) { this.waterFilter = f; }
public void setSkyControl(SkyControl sc) { this.skyControl = sc; }
public void setLightScatterFilter(com.jme3.post.filters.LightScatteringFilter f) { this.lightScatterFilter = f; }
public void setDayTime(DayTime dt) { this.dayTime = dt; }
public Weather getActiveWeather() { return active; }
public float getWindSpeed() { return windSpeed; }
/**
* Setzt das Wetter sofort; Werte interpolieren sanft zum neuen Ziel.
* Der automatische Wechsel-Timer wird zurückgesetzt.
*/
public void forceWeather(Weather w) {
active = w;
changeTimer = 90f + FastMath.nextRandomFloat() * 150f;
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
log.info("[Weather] forceWeather → {}", w);
}
public Weather getActiveWeather() { return active; }
public float getWindSpeed() { return windSpeed; }
public float getWindAngle() { return windAngle; }
public float getFogDensity() { return fogDensity; }
public float getChangeTimer() { return changeTimer; }
public float getRainIntensity() { return RAIN_INTENSITY[active.ordinal()]; }
/** Normierte Wolkenbedeckung 0..1. */
public float getCloudCover() { return FastMath.clamp((cloudOpacity + cloudOvercastOpacity) * 0.5f, 0f, 1f); }
public Vector3f getWindDirection() {
return new Vector3f(FastMath.sin(windAngle), 0f, FastMath.cos(windAngle));
}
/**
* Nächsten Wetterzustand sofort auslösen (wie automatischer Übergang).
* Gibt den neuen Zustand zurück.
*/
public Weather triggerNext() {
pickNextWeather();
return active;
}
/**
* Wetter sofort setzen; Werte interpolieren sanft zum neuen Ziel.
* Automatischer Wechsel-Timer wird zurückgesetzt.
*/
public void forceWeather(Weather w) {
active = w;
changeTimer = randomDuration(w);
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
log.info("[Weather] forceWeather → {}", w);
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override protected void initialize(Application app) {}
@Override protected void cleanup(Application app) {}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
protected void initialize(Application app) {
load();
}
@Override
protected void cleanup(Application app) {
save();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void update(float tpf) {
changeTimer -= tpf;
if (changeTimer <= 0f) {
changeTimer = 90f + FastMath.nextRandomFloat() * 150f;
pickNextWeather();
}
saveTimer -= tpf;
if (saveTimer <= 0f) {
saveTimer = 60f;
save();
}
int i = active.ordinal();
fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f);
fogDistance = approach(fogDistance, FOG_DISTANCE[i] * viewDistanceFactor, tpf * 0.025f);
windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f);
waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f);
waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f);
waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f);
waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f);
foamIntensity = approach(foamIntensity, FOAM_INTENSITY[i], tpf * 0.020f);
cloudOpacity = approach(cloudOpacity, CLOUD_OPACITY[i], tpf * 0.025f);
windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f);
fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f);
fogDistance = approach(fogDistance, FOG_DISTANCE[i] * viewDistanceFactor, tpf * 0.025f);
windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f);
waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f);
waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f);
waveScale = approach(waveScale, WAVE_SCALE[i], tpf * 0.020f);
waterTrans = approach(waterTrans, WATER_TRANS[i], tpf * 0.025f);
foamIntensity = approach(foamIntensity, FOAM_INTENSITY[i],tpf * 0.020f);
cloudOpacity = approach(cloudOpacity, CLOUD_OPACITY[i], tpf * 0.025f);
cloudOvercastOpacity = approach(cloudOvercastOpacity, OVERCAST_OPACITY[i], tpf * 0.025f);
windAngle = approachAngle(windAngle, windAngleTgt, tpf * 0.012f);
fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f);
waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f);
// UV-Offset in Windrichtung akkumulieren
float scrollRate = windSpeed * 0.0001f;
cloudOffsetU += FastMath.sin(windAngle) * scrollRate * tpf;
cloudOffsetV += FastMath.cos(windAngle) * scrollRate * tpf;
fogColor.interpolateLocal(FOG_COLOR[i], tpf * 0.025f);
waterColor.interpolateLocal(WATER_COLOR[i], tpf * 0.020f);
deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f);
if (fogFilter != null) {
@@ -143,7 +331,6 @@ public class WeatherState extends BaseAppState {
fogFilter.setFogDistance(fogDistance);
fogFilter.setFogColor(fogColor.clone());
}
if (waterFilter != null) {
waterFilter.setSpeed(waveSpeed);
waterFilter.setMaxAmplitude(waveAmp);
@@ -153,30 +340,117 @@ public class WeatherState extends BaseAppState {
waterFilter.setWaterColor(waterColor.clone());
waterFilter.setDeepWaterColor(deepWaterColor.clone());
waterFilter.setWindDirection(
new Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle)));
new Vector2f(FastMath.sin(windAngle), FastMath.cos(windAngle)));
}
if (lightScatterFilter != null) {
// Max 0.08 um Gras-Streifen zu vermeiden; Nebel/Wolken reduzieren weiter
float ld = Math.max(0f, 0.08f - fogDensity * 0.3f);
lightScatterFilter.setLightDensity(ld);
}
}
if (skyControl != null) {
skyControl.getCloudLayer(0).setOpacity(cloudOpacity);
// ── Übergänge ─────────────────────────────────────────────────────────────
private void pickNextWeather() {
Weather[] opts = TRANSITIONS[active.ordinal()];
Weather next = opts[(int) (FastMath.nextRandomFloat() * opts.length)];
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
active = next;
changeTimer = randomDuration(next);
log.info("[Weather] → {} ({}s)", active, (int) changeTimer);
}
private static float randomDuration(Weather w) {
int i = w.ordinal();
return DURATION_MIN[i] + FastMath.nextRandomFloat() * (DURATION_MAX[i] - DURATION_MIN[i]);
}
// ── Persistenz ────────────────────────────────────────────────────────────
private static File saveFile() {
File dir = new File(System.getProperty("user.home"), ".blight");
dir.mkdirs();
return new File(dir, "weather.properties");
}
public void save() {
Properties p = new Properties();
p.setProperty("weather", active.name());
if (dayTime != null) p.setProperty("timeOfDay", String.valueOf(dayTime.getTimeOfDay()));
p.setProperty("changeTimer", String.valueOf(changeTimer));
p.setProperty("windAngle", String.valueOf(windAngle));
p.setProperty("windAngleTgt", String.valueOf(windAngleTgt));
p.setProperty("fogDensity", String.valueOf(fogDensity));
p.setProperty("fogDistance", String.valueOf(fogDistance));
p.setProperty("windSpeed", String.valueOf(windSpeed));
p.setProperty("waveSpeed", String.valueOf(waveSpeed));
p.setProperty("waveAmp", String.valueOf(waveAmp));
p.setProperty("waveScale", String.valueOf(waveScale));
p.setProperty("waterTrans", String.valueOf(waterTrans));
p.setProperty("foamIntensity", String.valueOf(foamIntensity));
p.setProperty("cloudOpacity", String.valueOf(cloudOpacity));
p.setProperty("fogColorR", String.valueOf(fogColor.r));
p.setProperty("fogColorG", String.valueOf(fogColor.g));
p.setProperty("fogColorB", String.valueOf(fogColor.b));
p.setProperty("waterColorR", String.valueOf(waterColor.r));
p.setProperty("waterColorG", String.valueOf(waterColor.g));
p.setProperty("waterColorB", String.valueOf(waterColor.b));
p.setProperty("deepWaterR", String.valueOf(deepWaterColor.r));
p.setProperty("deepWaterG", String.valueOf(deepWaterColor.g));
p.setProperty("deepWaterB", String.valueOf(deepWaterColor.b));
try (FileWriter fw = new FileWriter(saveFile())) {
p.store(fw, "Blight weather state");
} catch (Exception e) {
log.warn("[Weather] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
private void load() {
File f = saveFile();
if (!f.exists()) return;
Properties p = new Properties();
try (FileReader fr = new FileReader(f)) {
p.load(fr);
active = Weather.valueOf(p.getProperty("weather", "SUNNY"));
changeTimer = Float.parseFloat(p.getProperty("changeTimer", "180"));
windAngle = Float.parseFloat(p.getProperty("windAngle", "0"));
windAngleTgt = Float.parseFloat(p.getProperty("windAngleTgt", "0.4"));
fogDensity = Float.parseFloat(p.getProperty("fogDensity", String.valueOf(FOG_DENSITY[0])));
fogDistance = Float.parseFloat(p.getProperty("fogDistance", String.valueOf(FOG_DISTANCE[0])));
windSpeed = Float.parseFloat(p.getProperty("windSpeed", String.valueOf(WIND_SPEED[0])));
waveSpeed = Float.parseFloat(p.getProperty("waveSpeed", String.valueOf(WAVE_SPEED[0])));
waveAmp = Float.parseFloat(p.getProperty("waveAmp", String.valueOf(WAVE_AMP[0])));
waveScale = Float.parseFloat(p.getProperty("waveScale", String.valueOf(WAVE_SCALE[0])));
waterTrans = Float.parseFloat(p.getProperty("waterTrans", String.valueOf(WATER_TRANS[0])));
foamIntensity = Float.parseFloat(p.getProperty("foamIntensity", String.valueOf(FOAM_INTENSITY[0])));
cloudOpacity = Float.parseFloat(p.getProperty("cloudOpacity", String.valueOf(CLOUD_OPACITY[0])));
fogColor.set(
Float.parseFloat(p.getProperty("fogColorR", String.valueOf(FOG_COLOR[0].r))),
Float.parseFloat(p.getProperty("fogColorG", String.valueOf(FOG_COLOR[0].g))),
Float.parseFloat(p.getProperty("fogColorB", String.valueOf(FOG_COLOR[0].b))),
1f);
waterColor.set(
Float.parseFloat(p.getProperty("waterColorR", String.valueOf(WATER_COLOR[0].r))),
Float.parseFloat(p.getProperty("waterColorG", String.valueOf(WATER_COLOR[0].g))),
Float.parseFloat(p.getProperty("waterColorB", String.valueOf(WATER_COLOR[0].b))),
1f);
deepWaterColor.set(
Float.parseFloat(p.getProperty("deepWaterR", String.valueOf(DEEP_WATER_COLOR[0].r))),
Float.parseFloat(p.getProperty("deepWaterG", String.valueOf(DEEP_WATER_COLOR[0].g))),
Float.parseFloat(p.getProperty("deepWaterB", String.valueOf(DEEP_WATER_COLOR[0].b))),
1f);
String savedTime = p.getProperty("timeOfDay");
if (savedTime != null && dayTime != null) {
dayTime.setTimeOfDay(Float.parseFloat(savedTime));
}
log.info("[Weather] Geladen: {} (noch {}s)", active, (int) changeTimer);
} catch (Exception e) {
log.warn("[Weather] Laden fehlgeschlagen: {}", e.getMessage());
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
private void pickNextWeather() {
float r = FastMath.nextRandomFloat();
Weather next = r < 0.38f ? Weather.SUNNY
: r < 0.65f ? Weather.CLOUDY
: r < 0.82f ? Weather.OVERCAST
: r < 0.92f ? Weather.STORM
: Weather.FOG;
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
if (next != active) {
active = next;
log.info("[Weather] transitioning to {}", active);
}
}
private static float approach(float cur, float tgt, float alpha) {
return cur + (tgt - cur) * FastMath.clamp(alpha, 0f, 1f);
}

View File

@@ -224,6 +224,13 @@ public class WorldObjectsState extends BaseAppState {
mat.setFloat("WindStrength", strength);
}
}
RainState rs = getApplication().getStateManager().getState(RainState.class);
if (rs != null) {
float w = rs.getWetness();
for (Material mat : sceneLitMaterials) setWetnessIfSupported(mat, w);
for (Material mat : windMaterials) setWetnessIfSupported(mat, w);
}
}
private Spatial buildSpatial(PlacedModel m) {
@@ -536,4 +543,10 @@ public class WorldObjectsState extends BaseAppState {
// PhysicsSpace kann beim App-Shutdown bereits zerstört sein
}
}
private static void setWetnessIfSupported(Material mat, float w) {
if (mat != null && mat.getMaterialDef().getMaterialParam("Wetness") != null) {
mat.setFloat("Wetness", w);
}
}
}