diff --git a/blight-assets/src/main/resources/Textures/menu/button.png b/blight-assets/src/main/resources/Textures/menu/button.png index 7e70211..0d73467 100644 Binary files a/blight-assets/src/main/resources/Textures/menu/button.png and b/blight-assets/src/main/resources/Textures/menu/button.png differ diff --git a/blight-assets/src/main/resources/audio/static/screenshot_click.ogg b/blight-assets/src/main/resources/audio/static/screenshot_click.ogg new file mode 100644 index 0000000..355ffdd Binary files /dev/null and b/blight-assets/src/main/resources/audio/static/screenshot_click.ogg differ diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 937a4b1..6147594 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -1477,6 +1477,7 @@ public class EditorApp extends Application { saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S")); saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); }); MenuItem screenshotItem = new MenuItem("Screenshot"); + screenshotItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+F12")); screenshotItem.setOnAction(e -> takeEditorScreenshot()); MenuItem settingsItem = new MenuItem("Einstellungen…"); settingsItem.setOnAction(e -> openSettings()); @@ -6442,6 +6443,7 @@ public class EditorApp extends Application { } private void takeEditorScreenshot() { + playScreenshotClick(); WritableImage image = primaryStage.getScene().snapshot(null); String timestamp = java.time.LocalDateTime.now() .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); @@ -6456,6 +6458,34 @@ public class EditorApp extends Application { } } + private void playScreenshotClick() { + Thread t = new Thread(() -> { + try { + int sr = 44100; + int frames = (int) (sr * 0.08); + byte[] buf = new byte[frames * 2]; + java.util.Random rng = new java.util.Random(); + for (int i = 0; i < frames; i++) { + double env = Math.exp(-i / (sr * 0.012)); + double s = (rng.nextGaussian() * 0.55 + Math.sin(2 * Math.PI * 3200.0 * i / sr) * 0.45) * env * 0.85; + short val = (short) Math.max(-32767, Math.min(32767, s * 32767)); + buf[i * 2] = (byte) (val & 0xff); + buf[i * 2 + 1] = (byte) ((val >> 8) & 0xff); + } + javax.sound.sampled.AudioFormat fmt = new javax.sound.sampled.AudioFormat(sr, 16, 1, true, false); + javax.sound.sampled.DataLine.Info info = new javax.sound.sampled.DataLine.Info(javax.sound.sampled.SourceDataLine.class, fmt); + javax.sound.sampled.SourceDataLine line = (javax.sound.sampled.SourceDataLine) javax.sound.sampled.AudioSystem.getLine(info); + line.open(fmt); + line.start(); + line.write(buf, 0, buf.length); + line.drain(); + line.close(); + } catch (Exception ignored) {} + }, "screenshot-click"); + t.setDaemon(true); + t.start(); + } + private void handleTextureImport(javafx.stage.Window owner) { FileChooser fc = new FileChooser(); fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)"); diff --git a/blight-game/src/main/java/de/blight/game/BlightGame.java b/blight-game/src/main/java/de/blight/game/BlightGame.java index b6da3e9..b050767 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -50,6 +50,7 @@ public class BlightGame extends SimpleApplication { private AudioSettings audioSettings; private ScreenshotAppState screenshotState; private Path screenshotDir; + private com.jme3.audio.AudioNode screenshotClickSound; private WorldScene worldScene; private ConfigScreen configScreen; private GraphicsScreen graphicsScreen; @@ -380,8 +381,12 @@ public class BlightGame extends SimpleApplication { try { screenshotDir = BlightHome.resolve("screenshots"); Files.createDirectories(screenshotDir); - screenshotState = new ScreenshotAppState(screenshotDir + File.separator, "screenshot"); + screenshotState = new ScreenshotAppState("", "screenshot"); stateManager.attach(screenshotState); + screenshotClickSound = new com.jme3.audio.AudioNode(assetManager, "audio/static/screenshot_click.ogg", com.jme3.audio.AudioData.DataType.Buffer); + screenshotClickSound.setPositional(false); + screenshotClickSound.setLooping(false); + screenshotClickSound.setVolume(1f); log.info("Screenshots werden gespeichert in: {}", screenshotDir.toAbsolutePath()); } catch (IOException e) { log.warn("Screenshot-Verzeichnis konnte nicht angelegt werden", e); @@ -389,8 +394,12 @@ public class BlightGame extends SimpleApplication { inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12)); inputManager.addListener((ActionListener) (name, isPressed, tpf) -> { if (isPressed && screenshotState != null) { - log.info("[Screenshot] Speichere in: {}", screenshotDir.toAbsolutePath()); + String ts = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); + screenshotState.setFileName(screenshotDir + java.io.File.separator + "blight_" + ts + "_"); + log.info("[Screenshot] Speichere: blight_{}.png", ts); screenshotState.takeScreenshot(); + if (screenshotClickSound != null) screenshotClickSound.playInstance(); } }, "Screenshot"); diff --git a/blight-game/src/main/java/de/blight/game/config/NinePatch.java b/blight-game/src/main/java/de/blight/game/config/NinePatch.java index 5beb6d9..a55ce17 100644 --- a/blight-game/src/main/java/de/blight/game/config/NinePatch.java +++ b/blight-game/src/main/java/de/blight/game/config/NinePatch.java @@ -16,6 +16,8 @@ import com.jme3.texture.image.ColorSpace; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.awt.image.ConvolveOp; +import java.awt.image.Kernel; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -127,6 +129,8 @@ public class NinePatch { // Guide-Pixel durch Rahmen-Nachbarfarbe ersetzen stripGuides(img, w, h, l - 1, w - r, t - 1, h - b); + img = gaussianBlur(img, 0.0f); + Texture2D tex = toJmeTexture(img, w, h); return new NinePatch(assets, tex, w, h, l, r, t, b); } @@ -219,6 +223,26 @@ public class NinePatch { return 0; // Fallback: transparent } + /** + * Weicher Gauss-Blur (3×3-Kernel) auf das Bild – glättet Übergänge an Patch-Grenzen. + * sigma=0 deaktiviert den Blur. + */ + private static BufferedImage gaussianBlur(BufferedImage img, float sigma) { + if (sigma <= 0f) { return img; } + float s2 = 2f * sigma * sigma; + float[] data = { + (float) Math.exp(-2 / s2), (float) Math.exp(-1 / s2), (float) Math.exp(-2 / s2), + (float) Math.exp(-1 / s2), 1f, (float) Math.exp(-1 / s2), + (float) Math.exp(-2 / s2), (float) Math.exp(-1 / s2), (float) Math.exp(-2 / s2) + }; + float sum = 0; for (float v : data) sum += v; + for (int i = 0; i < data.length; i++) data[i] /= sum; + ConvolveOp op = new ConvolveOp(new Kernel(3, 3, data), ConvolveOp.EDGE_NO_OP, null); + BufferedImage out = new BufferedImage(img.getWidth(), img.getHeight(), img.getType()); + op.filter(img, out); + return out; + } + /** * Konvertiert ein BufferedImage in eine JME3-Textur. * Zeilen werden von unten nach oben gelesen (entspricht JME3-Y-Flip beim PNG-Laden). @@ -266,7 +290,7 @@ public class NinePatch { */ public Node build(float x, float y, float w, float h, float z) { tex.setWrap(Texture.WrapMode.EdgeClamp); - tex.setMagFilter(Texture.MagFilter.Nearest); + tex.setMagFilter(Texture.MagFilter.Bilinear); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setTexture("ColorMap", tex); @@ -282,22 +306,75 @@ public class NinePatch { float sx1 = x + l, sx2 = x + w - r; float sy1 = y + b, sy2 = y + h - t; + // Kachelgröße des Mittelbereichs in Bildschirmpixeln + float cw = (u2 - u1) * texW; + float ch = (v2 - v1) * texH; + Node n = new Node("ninepatch"); - // Reihe unten - n.attachChild(q(mat, x, y, l, b, 0, 0, u1, v1, z)); - n.attachChild(q(mat, sx1, y, sx2-sx1, b, u1, 0, u2, v1, z)); - n.attachChild(q(mat, sx2, y, r, b, u2, 0, 1, v1, z)); - // Reihe mitte - n.attachChild(q(mat, x, sy1, l, sy2-sy1, 0, v1, u1, v2, z)); - n.attachChild(q(mat, sx1, sy1, sx2-sx1, sy2-sy1, u1, v1, u2, v2, z)); - n.attachChild(q(mat, sx2, sy1, r, sy2-sy1, u2, v1, 1, v2, z)); - // Reihe oben - n.attachChild(q(mat, x, sy2, l, t, 0, v2, u1, 1, z)); - n.attachChild(q(mat, sx1, sy2, sx2-sx1, t, u1, v2, u2, 1, z)); - n.attachChild(q(mat, sx2, sy2, r, t, u2, v2, 1, 1, z)); + // Reihe unten – Ecken fix, untere Kante Mirror-X + n.attachChild(q(mat, x, y, l, b, 0, 0, u1, v1, z)); + tileHEdge(n, mat, sx1, y, sx2, b, u1, u2, 0, v1, cw, texW, z); + n.attachChild(q(mat, sx2, y, r, b, u2, 0, 1, v1, z)); + // Reihe mitte – Seiten Mirror-Y, Mitte Mirror-X+Y + tileVEdge(n, mat, x, sy1, l, sy2, 0, u1, v1, v2, ch, texH, z); + tileCenterQuads(n, mat, sx1, sy1, sx2, sy2, u1, v1, u2, v2, cw, ch, texW, texH, z); + tileVEdge(n, mat, sx2, sy1, r, sy2, u2, 1f, v1, v2, ch, texH, z); + // Reihe oben – Ecken fix, obere Kante Mirror-X + n.attachChild(q(mat, x, sy2, l, t, 0, v2, u1, 1f, z)); + tileHEdge(n, mat, sx1, sy2, sx2, t, u1, u2, v2, 1f, cw, texW, z); + n.attachChild(q(mat, sx2, sy2, r, t, u2, v2, 1f, 1f, z)); return n; } + /** Mitte: Mirror-Repeat in X und Y. */ + private static void tileCenterQuads(Node n, Material mat, + float sx1, float sy1, float sx2, float sy2, + float u1, float v1, float u2, float v2, + float cw, float ch, + float texW, float texH, float z) { + int iz = 0; + for (float py = sy1; py < sy2; py += ch, iz++) { + float th = Math.min(ch, sy2 - py); + float vA = (iz & 1) == 0 ? v1 : v2; + float vB = (iz & 1) == 0 ? v1+th/texH : v2-th/texH; + int ix = 0; + for (float px = sx1; px < sx2; px += cw, ix++) { + float tw = Math.min(cw, sx2 - px); + float uA = (ix & 1) == 0 ? u1 : u2; + float uB = (ix & 1) == 0 ? u1+tw/texW : u2-tw/texW; + n.attachChild(q(mat, px, py, tw, th, uA, vA, uB, vB, z)); + } + } + } + + /** Horizontaler Randstreifen (oben/unten): Mirror-Repeat nur in X. */ + private static void tileHEdge(Node n, Material mat, + float sx1, float py, float sx2, float ph, + float u1, float u2, float vMin, float vMax, + float cw, float texW, float z) { + int ix = 0; + for (float px = sx1; px < sx2; px += cw, ix++) { + float tw = Math.min(cw, sx2 - px); + float uA = (ix & 1) == 0 ? u1 : u2; + float uB = (ix & 1) == 0 ? u1+tw/texW : u2-tw/texW; + n.attachChild(q(mat, px, py, tw, ph, uA, vMin, uB, vMax, z)); + } + } + + /** Vertikaler Randstreifen (links/rechts): Mirror-Repeat nur in Y. */ + private static void tileVEdge(Node n, Material mat, + float px, float sy1, float pw, float sy2, + float uMin, float uMax, float v1, float v2, + float ch, float texH, float z) { + int iz = 0; + for (float py = sy1; py < sy2; py += ch, iz++) { + float th = Math.min(ch, sy2 - py); + float vA = (iz & 1) == 0 ? v1 : v2; + float vB = (iz & 1) == 0 ? v1+th/texH : v2-th/texH; + n.attachChild(q(mat, px, py, pw, th, uMin, vA, uMax, vB, z)); + } + } + private static Geometry q(Material mat, float x, float y, float w, float h, float uMin, float vMin, float uMax, float vMax, diff --git a/blight-map/src/main/map/chunks/voxel_17_0_08.blvc b/blight-map/src/main/map/chunks/voxel_17_0_08.blvc index 0972d31..bc33783 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_08.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_08.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_09.blvc b/blight-map/src/main/map/chunks/voxel_17_0_09.blvc index dab3012..5d268c8 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_0_09.blvc and b/blight-map/src/main/map/chunks/voxel_17_0_09.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc index 798d1b2..b3323cb 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc index 0038154..1540be5 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_09.blvc differ