diff --git a/blight-assets/src/main/resources/Textures/menu/button_quit.png b/blight-assets/src/main/resources/Textures/menu/button_quit.png index 967bcf4..4830c98 100644 Binary files a/blight-assets/src/main/resources/Textures/menu/button_quit.png and b/blight-assets/src/main/resources/Textures/menu/button_quit.png 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 7365b7b..9491272 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -11326,8 +11326,10 @@ public class EditorApp extends Application { var clip = clipCombo.getValue(); if (action != null && clip != null) { String entry = action.name() + " → " + clip; - if (!animSetActionListView.getItems().contains(entry)) + if (!animSetActionListView.getItems().contains(entry)) { animSetActionListView.getItems().add(entry); + autoSaveAnimSet(); + } } }); } diff --git a/blight-game/src/main/java/de/blight/game/config/MenuScreen.java b/blight-game/src/main/java/de/blight/game/config/MenuScreen.java index 235c39d..58e2eb5 100644 --- a/blight-game/src/main/java/de/blight/game/config/MenuScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/MenuScreen.java @@ -18,6 +18,12 @@ import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; import com.jme3.scene.Node; import com.jme3.scene.shape.Quad; +import com.jme3.texture.Image; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; +import com.jme3.util.BufferUtils; +import java.nio.ByteBuffer; import de.blight.game.post.GaussianBlurFilter; import de.blight.game.scene.WorldScene; @@ -65,8 +71,15 @@ public abstract class MenuScreen extends BaseAppState { private final String MAP_CLICK = "_MsClick_" + System.identityHashCode(this); - private record Btn(float x, float y, float w, float h, Runnable action) {} + private record Btn(float x, float y, float w, float h, Runnable action, + Node patchNode, BitmapText label) {} private final List buttons = new ArrayList<>(); + private Btn pressedBtn = null; + private Btn hoveredBtn = null; + private Node hoverGlow = null; + private Texture2D glowTex = null; + private static final float PRESS_DX = 1f, PRESS_DY = -2f; + private static final float GLOW_PAD = 20f; // ── Lifecycle ───────────────────────────────────────────────────────────── @@ -114,6 +127,9 @@ public abstract class MenuScreen extends BaseAppState { if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } panel = null; buttons.clear(); + pressedBtn = null; + hoveredBtn = null; + if (hoverGlow != null) { hoverGlow.removeFromParent(); hoverGlow = null; } if (withBlur()) removeBlur(); } @@ -171,7 +187,8 @@ public abstract class MenuScreen extends BaseAppState { case ARROW -> NinePatch.buttonArrow(assets); default -> NinePatch.button(assets); }; - parent.attachChild(patch.build(x, y, w, h, 0f)); + Node patchNode = patch.build(x, y, w, h, 0f); + parent.attachChild(patchNode); ColorRGBA col = (style == BtnStyle.DISABLED) ? UiTheme.COL_MUTED : UiTheme.COL_WHITE; BitmapText lbl = crispText(label, UiTheme.FONT_LABEL, col); @@ -180,21 +197,108 @@ public abstract class MenuScreen extends BaseAppState { parent.attachChild(lbl); if (style != BtnStyle.DISABLED && action != null) { - buttons.add(new Btn(x, y, w, h, action)); + buttons.add(new Btn(x, y, w, h, action, patchNode, lbl)); } } + // ── Update-Loop (Hover) ─────────────────────────────────────────────────── + + @Override + public void update(float tpf) { + if (buttons.isEmpty() || panel == null) return; + float[] v = toVirtual(app.getInputManager().getCursorPosition()); + Btn hovered = null; + for (Btn b : buttons) { + if (v[0] >= b.x() && v[0] <= b.x() + b.w() + && v[1] >= b.y() && v[1] <= b.y() + b.h()) { + hovered = b; + break; + } + } + if (hovered != hoveredBtn) { + if (hoverGlow != null) { hoverGlow.removeFromParent(); hoverGlow = null; } + hoveredBtn = hovered; + if (hoveredBtn != null) { + hoverGlow = buildHoverGlow(hoveredBtn); + panel.attachChild(hoverGlow); + } + } + if (hoverGlow != null) { + float dx = (pressedBtn == hoveredBtn) ? PRESS_DX : 0f; + float dy = (pressedBtn == hoveredBtn) ? PRESS_DY : 0f; + hoverGlow.setLocalTranslation(hoveredBtn.x() + dx, hoveredBtn.y() + dy, 0f); + } + } + + private Node buildHoverGlow(Btn b) { + Geometry q = new Geometry("glow", + new Quad(b.w() + GLOW_PAD * 2f, b.h() + GLOW_PAD * 2f)); + Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + m.setTexture("ColorMap", getGlowTex()); + m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive); + q.setQueueBucket(RenderQueue.Bucket.Gui); + q.setMaterial(m); + // -GLOW_PAD damit der Glow um den Button herausragt; Node selbst wird auf Button-Pos gesetzt + q.setLocalTranslation(-GLOW_PAD, -GLOW_PAD, -0.5f); + Node g = new Node("hover-glow"); + g.attachChild(q); + return g; + } + + private Texture2D getGlowTex() { + if (glowTex != null) return glowTex; + int W = 64, H = 64; + ByteBuffer buf = BufferUtils.createByteBuffer(W * H * 4); + for (int py = 0; py < H; py++) { + for (int px = 0; px < W; px++) { + float u = (px + 0.5f) / W; + float v = (py + 0.5f) / H; + float a = glowFade(u) * glowFade(v); + buf.put((byte) 13); // R ≈ 0.05 × 255 + buf.put((byte) 255); // G = neon + buf.put((byte) 51); // B ≈ 0.20 × 255 + buf.put((byte)(int)(a * 220f)); // A peak ≈ 86 % + } + } + buf.flip(); + glowTex = new Texture2D(new Image(Image.Format.RGBA8, W, H, buf, ColorSpace.Linear)); + glowTex.setMagFilter(Texture.MagFilter.Bilinear); + glowTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); + return glowTex; + } + + private static float glowFade(float t) { + float d = Math.abs(t - 0.5f) * 2f; // 0 = Mitte, 1 = Rand + float x = 1f - Math.min(1f, d); + return x * x * (3f - 2f * x); // smoothstep + } + // ── Klick-Listener ──────────────────────────────────────────────────────── private final ActionListener clickListener = (name, pressed, tpf) -> { - if (!pressed) return; float[] v = toVirtual(app.getInputManager().getCursorPosition()); - onScreenClick(v); - for (Btn b : buttons) { - if (v[0] >= b.x() && v[0] <= b.x() + b.w() - && v[1] >= b.y() && v[1] <= b.y() + b.h()) { - b.action().run(); - return; + if (pressed) { + onScreenClick(v); + for (Btn b : buttons) { + if (v[0] >= b.x() && v[0] <= b.x() + b.w() + && v[1] >= b.y() && v[1] <= b.y() + b.h()) { + pressedBtn = b; + b.patchNode().move(PRESS_DX, PRESS_DY, 0f); + b.label().move(PRESS_DX, PRESS_DY, 0f); + return; + } + } + } else { + if (pressedBtn != null) { + pressedBtn.patchNode().move(-PRESS_DX, -PRESS_DY, 0f); + pressedBtn.label().move(-PRESS_DX, -PRESS_DY, 0f); + Btn released = pressedBtn; + pressedBtn = null; + // Aktion nur auslösen wenn Maus noch über dem Button + if (v[0] >= released.x() && v[0] <= released.x() + released.w() + && v[1] >= released.y() && v[1] <= released.y() + released.h()) { + released.action().run(); + } } } }; 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 a55ce17..979d0d7 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 @@ -171,6 +171,12 @@ public class NinePatch { int t = ty >= 0 ? ty + 1 : h / 4; int b = by >= 0 ? h - by : h / 4; + // Sicherstellen, dass Mittelbereich mindestens 1 Pixel groß ist. + // Fehlerhafte Guide-Erkennung (Pixel mit zufällig passendem Alpha) kann + // sonst l+r >= w oder t+b >= h erzeugen → Endlosschleife in den Tiling-Methoden. + if (l + r >= w) { l = w / 4; r = w / 4; } + if (t + b >= h) { t = h / 4; b = h / 4; } + return new int[]{l, r, t, b}; } @@ -182,12 +188,16 @@ public class NinePatch { * ty, by: Zeilenindizes der oberen/unteren Guide-Linie (PNG-Koordinaten) */ private static void stripGuides(BufferedImage img, int w, int h, int lx, int rx, int ty, int by) { - // Snapshot der Guide-Positionen (vor Modifikation) + // Snapshot der Guide-Positionen (vor Modifikation). + // Als Guide zählen: exakte Guide-Alphas ODER semi-transparente Pixel (alpha < 200) + // auf den bekannten Guide-Zeilen/-Spalten — entsteht durch Anti-Aliasing beim Malen. boolean[][] isGuide = new boolean[h][w]; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int a = (img.getRGB(x, y) >> 24) & 0xff; - isGuide[y][x] = GUIDE_ALPHAS.contains(a); + boolean exactGuide = GUIDE_ALPHAS.contains(a); + boolean onGuideLine = (x == lx || x == rx || y == ty || y == by); + isGuide[y][x] = exactGuide || (onGuideLine && a < 200 && a > 0); } } @@ -332,6 +342,7 @@ public class NinePatch { float u1, float v1, float u2, float v2, float cw, float ch, float texW, float texH, float z) { + if (cw <= 0 || ch <= 0) return; int iz = 0; for (float py = sy1; py < sy2; py += ch, iz++) { float th = Math.min(ch, sy2 - py); @@ -352,6 +363,7 @@ public class NinePatch { float sx1, float py, float sx2, float ph, float u1, float u2, float vMin, float vMax, float cw, float texW, float z) { + if (cw <= 0) return; int ix = 0; for (float px = sx1; px < sx2; px += cw, ix++) { float tw = Math.min(cw, sx2 - px); @@ -366,6 +378,7 @@ public class NinePatch { float px, float sy1, float pw, float sy2, float uMin, float uMax, float v1, float v2, float ch, float texH, float z) { + if (ch <= 0) return; int iz = 0; for (float py = sy1; py < sy2; py += ch, iz++) { float th = Math.min(ch, sy2 - py);