Weiter am Wettersystem gearbeitet, ordentliche Wolken und Regen etc ergänzt
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(", "));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
126
blight-game/src/main/java/de/blight/game/state/CursorState.java
Normal file
126
blight-game/src/main/java/de/blight/game/state/CursorState.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
310
blight-game/src/main/java/de/blight/game/state/RainState.java
Normal file
310
blight-game/src/main/java/de/blight/game/state/RainState.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user