From 2c88b7e313a727854c709d397a923b1eacdb0609 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 26 Jul 2026 18:26:56 +0200 Subject: [PATCH] gitignore-Fix: Quell-Paket config/ nicht mehr ignorieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **/config/ hat fälschlicherweise auch de.blight.game.config (9 Java-Dateien) ignoriert. Ersetze durch explizite Regeln nur für Laufzeit-Konfigurationen (root/config/, blight-editor/config/, blight-game/config/). Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 6 +- .../de/blight/game/config/AudioScreen.java | 145 ++++++++ .../de/blight/game/config/AudioSettings.java | 9 + .../game/config/AudioSettingsStore.java | 40 +++ .../de/blight/game/config/MainMenuState.java | 53 +++ .../de/blight/game/config/MenuCanvas.java | 107 ++++++ .../de/blight/game/config/MenuScreen.java | 249 ++++++++++++++ .../java/de/blight/game/config/NinePatch.java | 324 ++++++++++++++++++ .../de/blight/game/config/OverlayState.java | 247 +++++++++++++ .../java/de/blight/game/config/UiTheme.java | 228 ++++++++++++ 10 files changed, 1407 insertions(+), 1 deletion(-) create mode 100644 blight-game/src/main/java/de/blight/game/config/AudioScreen.java create mode 100644 blight-game/src/main/java/de/blight/game/config/AudioSettings.java create mode 100644 blight-game/src/main/java/de/blight/game/config/AudioSettingsStore.java create mode 100644 blight-game/src/main/java/de/blight/game/config/MainMenuState.java create mode 100644 blight-game/src/main/java/de/blight/game/config/MenuCanvas.java create mode 100644 blight-game/src/main/java/de/blight/game/config/MenuScreen.java create mode 100644 blight-game/src/main/java/de/blight/game/config/NinePatch.java create mode 100644 blight-game/src/main/java/de/blight/game/config/OverlayState.java create mode 100644 blight-game/src/main/java/de/blight/game/config/UiTheme.java diff --git a/.gitignore b/.gitignore index 335bab0..aa6b2aa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,11 @@ # Gradle build output **/build/ -**/config/ + +# Laufzeit-Konfigurationen (keine Quell-Pakete!) +/config/ +blight-editor/config/ +blight-game/config/ # Eclipse project files .project diff --git a/blight-game/src/main/java/de/blight/game/config/AudioScreen.java b/blight-game/src/main/java/de/blight/game/config/AudioScreen.java new file mode 100644 index 0000000..e87f1da --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/AudioScreen.java @@ -0,0 +1,145 @@ +package de.blight.game.config; + +import com.jme3.font.BitmapText; +import com.jme3.math.ColorRGBA; +import com.jme3.scene.Node; +import de.blight.lang.TextResolver; + +public class AudioScreen extends MenuScreen { + + private static final int ROW_MASTER = 0; + private static final int ROW_MUSIC = 1; + private static final int ROW_SPEECH = 2; + private static final int ROW_EFFECTS = 3; + private static final int ROW_AMBIENT = 4; + private static final int ROW_COUNT = 5; + private static final int STEP_COUNT = 11; + + private final AudioSettings live; + private AudioSettings edit; + private final Runnable onClose; + + private final int[] stepIdx = new int[ROW_COUNT]; + + private final float arrW = 30f; + private final float cellW = 120f; + private final float cellH = 36f; + private final float[] cellX = new float[ROW_COUNT]; + private final float[] cellY = new float[ROW_COUNT]; + private final BitmapText[] valTexts = new BitmapText[ROW_COUNT]; + + public AudioScreen(AudioSettings live, Runnable onClose) { + this.live = live; + this.onClose = onClose; + } + + @Override + protected void onEnableExtras() { + edit = new AudioSettings(); + edit.master = live.master; + edit.music = live.music; + edit.speech = live.speech; + edit.effects = live.effects; + edit.ambient = live.ambient; + + stepIdx[ROW_MASTER] = toStepIdx(edit.master); + stepIdx[ROW_MUSIC] = toStepIdx(edit.music); + stepIdx[ROW_SPEECH] = toStepIdx(edit.speech); + stepIdx[ROW_EFFECTS] = toStepIdx(edit.effects); + stepIdx[ROW_AMBIENT] = toStepIdx(edit.ambient); + } + + @Override + protected void buildUI() { + initPanel(t("menu.audio.title")); + + String[] rowKeys = { + "menu.audio.row.master", "menu.audio.row.music", "menu.audio.row.speech", + "menu.audio.row.effects","menu.audio.row.ambient" + }; + + float lblX = CONT_X + 30f; + float vx = CONT_X + CONT_W - 240f; + float startY = CONT_Y + CONT_H - HDR_H - 58f; + float step = 56f; + + for (int i = 0; i < ROW_COUNT; i++) { + final int row = i; + float ry = startY - i * step; + + BitmapText lbl = crispText(t(rowKeys[i]), UiTheme.FONT_LABEL, UiTheme.COL_WHITE); + lbl.setLocalTranslation(lblX, ry + cellH - 4f, 0f); + panel.attachChild(lbl); + + addButton(panel, "<", vx - arrW - 6f, ry, arrW, cellH, + BtnStyle.ARROW, () -> cycle(row, -1)); + + panel.attachChild(NinePatch.button(assets).build(vx, ry, cellW, cellH, 0f)); + + addButton(panel, ">", vx + cellW + 6f, ry, arrW, cellH, + BtnStyle.ARROW, () -> cycle(row, +1)); + + BitmapText vt = crispText("", UiTheme.FONT_LABEL, UiTheme.COL_GOLD); + panel.attachChild(vt); + valTexts[i] = vt; + + cellX[i] = vx; + cellY[i] = ry; + } + + for (int i = 0; i < ROW_COUNT; i++) refreshText(i); + + float bw = 160f, bh = 42f; + float btnY = CONT_Y + 22f; + float saveX = CONT_X + CONT_W / 2f - bw - 10f; + float canX = CONT_X + CONT_W / 2f + 10f; + addButton(panel, t("menu.audio.btn.apply"), saveX, btnY, bw, bh, BtnStyle.SAVE, this::applyAndSave); + addButton(panel, t("menu.audio.btn.cancel"), canX, btnY, bw, bh, BtnStyle.QUIT, this::close); + } + + private void refreshText(int row) { + int pct = stepIdx[row] * 10; + BitmapText vt = valTexts[row]; + vt.setText(pct + "%"); + float tw = vt.getLineWidth() / getUiScale(); + vt.setLocalTranslation( + cellX[row] + (cellW - tw) / 2f, + cellY[row] + cellH - 4f, + 1f + ); + } + + private void cycle(int row, int dir) { + stepIdx[row] = Math.max(0, Math.min(STEP_COUNT - 1, stepIdx[row] + dir)); + float val = stepIdx[row] / 10f; + switch (row) { + case ROW_MASTER -> edit.master = val; + case ROW_MUSIC -> edit.music = val; + case ROW_SPEECH -> edit.speech = val; + case ROW_EFFECTS -> edit.effects = val; + case ROW_AMBIENT -> edit.ambient = val; + } + refreshText(row); + } + + private void applyAndSave() { + live.master = edit.master; + live.music = edit.music; + live.speech = edit.speech; + live.effects = edit.effects; + live.ambient = edit.ambient; + AudioSettingsStore.save(live); + close(); + } + + private void close() { + setEnabled(false); + if (onClose != null) onClose.run(); + } + + private static int toStepIdx(float v) { + return Math.max(0, Math.min(STEP_COUNT - 1, Math.round(v * 10f))); + } + + private static String t(String id) { return TextResolver.get().resolveId(id); } +} diff --git a/blight-game/src/main/java/de/blight/game/config/AudioSettings.java b/blight-game/src/main/java/de/blight/game/config/AudioSettings.java new file mode 100644 index 0000000..5e53245 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/AudioSettings.java @@ -0,0 +1,9 @@ +package de.blight.game.config; + +public class AudioSettings { + public float master = 1.0f; + public float music = 1.0f; + public float speech = 1.0f; + public float effects = 1.0f; + public float ambient = 0.5f; +} diff --git a/blight-game/src/main/java/de/blight/game/config/AudioSettingsStore.java b/blight-game/src/main/java/de/blight/game/config/AudioSettingsStore.java new file mode 100644 index 0000000..e0ac10f --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/AudioSettingsStore.java @@ -0,0 +1,40 @@ +package de.blight.game.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import de.blight.common.BlightHome; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.file.*; + +public class AudioSettingsStore { + + private static final Logger log = LoggerFactory.getLogger(AudioSettingsStore.class); + private static final Path FILE = BlightHome.resolve("config", "audio.json"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public static AudioSettings load() { + if (Files.exists(FILE)) { + try (Reader r = Files.newBufferedReader(FILE)) { + AudioSettings s = GSON.fromJson(r, AudioSettings.class); + return s != null ? s : new AudioSettings(); + } catch (IOException e) { + log.warn("audio.json konnte nicht geladen werden: {}", e.getMessage()); + } + } + return new AudioSettings(); + } + + public static void save(AudioSettings s) { + try { + Files.createDirectories(FILE.getParent()); + try (Writer w = Files.newBufferedWriter(FILE)) { + GSON.toJson(s, w); + } + } catch (IOException e) { + log.error("audio.json konnte nicht gespeichert werden: {}", e.getMessage()); + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/MainMenuState.java b/blight-game/src/main/java/de/blight/game/config/MainMenuState.java new file mode 100644 index 0000000..fd67261 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/MainMenuState.java @@ -0,0 +1,53 @@ +package de.blight.game.config; + +import com.jme3.font.BitmapText; +import com.jme3.math.ColorRGBA; +import com.jme3.scene.Node; +import de.blight.lang.TextResolver; + +public class MainMenuState extends MenuScreen { + + private static final ColorRGBA COL_TITLE = new ColorRGBA(0.85f, 0.70f, 0.35f, 1.00f); + + private final Runnable onNewGame; + private final Runnable onContinue; + private final Runnable onLoad; + private final Runnable onOptions; + private final Runnable onQuit; + + public MainMenuState(Runnable onNewGame, Runnable onContinue, Runnable onLoad, + Runnable onOptions, Runnable onQuit) { + this.onNewGame = onNewGame; + this.onContinue = onContinue; + this.onLoad = onLoad; + this.onOptions = onOptions; + this.onQuit = onQuit; + } + + @Override + protected void buildUI() { + initPanel("BLIGHT", COL_TITLE); + + addQuad(panel, CONT_X + 20f, CONT_Y + CONT_H - HDR_H - 10f, CONT_W - 40f, 1f, + new ColorRGBA(0.3f, 0.3f, 0.5f, 1f), -17f); + + String[] keys = { + "menu.main.btn.new_game", "menu.main.btn.continue", "menu.main.btn.load", + "menu.main.btn.options", "menu.main.btn.quit" + }; + Runnable[] acts = { onNewGame, onContinue, onLoad, onOptions, onQuit }; + + float bw = 260f, bh = 50f; + float bx = CONT_X + (CONT_W - bw) / 2f; + float startY = CONT_Y + CONT_H - HDR_H - 68f; + float step = 62f; + + for (int i = 0; i < 5; i++) { + boolean enabled = acts[i] != null; + BtnStyle style = !enabled ? BtnStyle.DISABLED : i == 4 ? BtnStyle.QUIT : BtnStyle.DEFAULT; + addButton(panel, t(keys[i]), bx, startY - i * step, bw, bh, style, acts[i]); + } + } + + private static String t(String id) { return TextResolver.get().resolveId(id); } +} diff --git a/blight-game/src/main/java/de/blight/game/config/MenuCanvas.java b/blight-game/src/main/java/de/blight/game/config/MenuCanvas.java new file mode 100644 index 0000000..e0a87e5 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/MenuCanvas.java @@ -0,0 +1,107 @@ +package de.blight.game.config; + +import com.jme3.asset.AssetManager; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.renderer.Camera; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; + +/** + * Gemeinsamer Helfer für alle Menü-Screens. + * + * Koordinatensystem: virtueller Referenzraum 1376 × 768. + * createCanvas() liefert einen skalierten Node, dessen Kinder in diesem Referenzraum + * positioniert werden und automatisch auf jeden Bildschirm skalieren. + * createBgLayer() / createPauseBgLayer() liefern ein dunkles Vollbild-Overlay + * im Transparent-Bucket (z=100, JME3 GUI-Kamera schaut in +Z → größere z = dahinter), + * sodass Panel-Elemente (z=1 bis 10) stets darüber rendern. + */ +public final class MenuCanvas { + + public static final float REF_W = 1376f; + public static final float REF_H = 768f; + + /** Rand auf jeder Seite als Anteil der Referenzgröße (0.10 = 10 % → 80 % Inhalt). */ + public static final float MARGIN_FRAC = 0.10f; + + public static final float CONT_X = REF_W * MARGIN_FRAC; + public static final float CONT_Y = REF_H * MARGIN_FRAC; + public static final float CONT_W = REF_W * (1f - 2f * MARGIN_FRAC); + public static final float CONT_H = REF_H * (1f - 2f * MARGIN_FRAC); + + private MenuCanvas() {} + + /** + * Canvas-Skalierungsfaktor (min-fit, kein Stretching). + * Hilfreich für crispText: Schriftgröße = virtualSize * getScale() + */ + public static float getScale(Camera cam) { + return Math.min(cam.getWidth() / REF_W, cam.getHeight() / REF_H); + } + + /** + * Skalierter Canvas-Node: Kinder in virtuellen Koordinaten (0..REF_W, 0..REF_H) + * erscheinen automatisch korrekt auf dem Bildschirm. + * Den Node direkt an guiNode hängen. + */ + public static Node createCanvas(Camera cam) { + float scale = Math.min(cam.getWidth() / REF_W, cam.getHeight() / REF_H); + float ox = (cam.getWidth() - REF_W * scale) / 2f; + float oy = (cam.getHeight() - REF_H * scale) / 2f; + Node n = new Node("menu-canvas"); + n.setLocalScale(scale, scale, 1f); + n.setLocalTranslation(ox, oy, 0f); + return n; + } + + /** + * Canvas für das Pause-Menü: keine Skalierung, in Bildschirmmitte zentriert. + */ + public static Node createFixedCanvas(Camera cam) { + float ox = (cam.getWidth() - REF_W) / 2f; + float oy = (cam.getHeight() - REF_H) / 2f; + Node n = new Node("menu-canvas-fixed"); + n.setLocalScale(1f, 1f, 1f); + n.setLocalTranslation(ox, oy, 0f); + return n; + } + + /** + * Dunkles Vollbild-Overlay (Transparent-Bucket, z=100). + * Liegt hinter allen Panel-Elementen (z=1 bis 10). + * Direkt an guiNode hängen (nicht an canvasNode). + */ + public static Node createBgLayer(AssetManager am, Camera cam) { + return darkOverlay(am, cam, "menu-bg-layer", 0.03f, 0.03f, 0.06f, 1f); + } + + /** + * Dunkles Vollbild-Overlay für die Pause-Ansicht (60% schwarz). + * Lässt die Spielwelt (gebluurt) durch und liegt hinter dem Panel. + * Direkt an guiNode hängen (nicht an canvasNode). + */ + public static Node createPauseBgLayer(AssetManager am, Camera cam) { + return darkOverlay(am, cam, "pause-bg-layer", 0f, 0f, 0f, 0.60f); + } + + // ── Privat ──────────────────────────────────────────────────────────────── + + private static Node darkOverlay(AssetManager am, Camera cam, String name, + float r, float g, float b, float a) { + float sw = cam.getWidth(), sh = cam.getHeight(); + Node layer = new Node(name); + Material mat = new Material(am, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(r, g, b, a)); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + Geometry geo = new Geometry("bg_overlay", new Quad(sw, sh)); + geo.setMaterial(mat); + geo.setQueueBucket(RenderQueue.Bucket.Gui); + geo.setLocalTranslation(0f, 0f, -100f); + layer.attachChild(geo); + return layer; + } +} 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 new file mode 100644 index 0000000..5bb9262 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/MenuScreen.java @@ -0,0 +1,249 @@ +package de.blight.game.config; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.post.FilterPostProcessor; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; +import de.blight.game.post.GaussianBlurFilter; +import de.blight.game.scene.WorldScene; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstrakte Basisklasse für alle Menü-Screens (Hauptmenü, Pause, Grafik, Audio, Tasten). + * + * Kümmert sich um: + * - Canvas + bgLayer Auf-/Abbau + * - NinePatch-Panel-Container mit dunklem Header + Titeltext + * - CrispText-Rendering (Counter-Scale für scharfe Schrift auf WQHD) + * - Button-Factory mit automatischer Klick-Erkennung + * - optionaler Blur-Support + * - toVirtual() / addQuad() Helfer + */ +public abstract class MenuScreen extends BaseAppState { + + public static final float HDR_H = 44f; + public static final float PAD = 14f; + + /** Abgeleitete Container-Koordinaten – geändert wird nur MenuCanvas.MARGIN_FRAC. */ + public static final float CONT_X = MenuCanvas.CONT_X; + public static final float CONT_Y = MenuCanvas.CONT_Y; + public static final float CONT_W = MenuCanvas.CONT_W; + public static final float CONT_H = MenuCanvas.CONT_H; + + public enum BtnStyle { DEFAULT, SAVE, QUIT, DISABLED, ARROW } + + protected SimpleApplication app; + protected AssetManager assets; + protected BitmapFont font; + protected Node guiNode; + protected Node canvasNode; + protected Node panel; + + private Node bgLayer; + private GaussianBlurFilter blurFilter; + private float uiScale; + + private final String MAP_CLICK = "_MsClick_" + System.identityHashCode(this); + + private record Btn(float x, float y, float w, float h, Runnable action) {} + private final List buttons = new ArrayList<>(); + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + protected void initialize(Application application) { + app = (SimpleApplication) application; + assets = app.getAssetManager(); + guiNode = app.getGuiNode(); + font = assets.loadFont("Interface/Fonts/Default.fnt"); + onInitMenu(); + } + + @Override + protected void onEnable() { + uiScale = MenuCanvas.getScale(app.getCamera()); + bgLayer = buildBgLayer(); + canvasNode = buildCanvas(); + if (bgLayer != null) guiNode.attachChild(bgLayer); + if (canvasNode != null) guiNode.attachChild(canvasNode); + + onEnableExtras(); + + buttons.clear(); + panel = new Node("ms-panel"); + buildUI(); + canvasNode.attachChild(panel); + + if (withBlur()) applyBlur(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addMapping(MAP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, MAP_CLICK); + } + + @Override + protected void onDisable() { + onDisableExtras(); + app.getInputManager().removeListener(clickListener); + if (app.getInputManager().hasMapping(MAP_CLICK)) { + app.getInputManager().deleteMapping(MAP_CLICK); + } + app.getInputManager().setCursorVisible(false); + if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; } + if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } + panel = null; + buttons.clear(); + if (withBlur()) removeBlur(); + } + + @Override + protected void cleanup(Application application) {} + + // ── Abstrakt ────────────────────────────────────────────────────────────── + + /** Inhalt aufbauen — addButton(), addQuad(), crispText() stehen bereit. */ + protected abstract void buildUI(); + + // ── Optionale Hooks ─────────────────────────────────────────────────────── + + protected void onInitMenu() {} + protected void onEnableExtras() {} + protected void onDisableExtras() {} + protected boolean withBlur() { return false; } + + protected Node buildBgLayer() { return MenuCanvas.createBgLayer(assets, app.getCamera()); } + protected Node buildCanvas() { return MenuCanvas.createCanvas(app.getCamera()); } + + // ── Panel-Initialisierung ───────────────────────────────────────────────── + + /** + * Baut NinePatch-Panel + dunklen Header + Titeltext auf. + * Muss am Anfang von buildUI() aufgerufen werden. + */ + /** Baut den 80%-Container mit Header für diesen Screen auf. */ + protected void initPanel(String title) { + initPanel(title, UiTheme.COL_WHITE); + } + + protected void initPanel(String title, ColorRGBA titleColor) { + panel.attachChild(NinePatch.container(assets).build(CONT_X, CONT_Y, CONT_W, CONT_H, -19f)); + addQuad(panel, CONT_X, CONT_Y + CONT_H - HDR_H, CONT_W, HDR_H, UiTheme.COL_HDR, -18f); + BitmapText t = crispText(title, UiTheme.FONT_HEADING, titleColor); + t.setLocalTranslation(CONT_X + PAD, + CONT_Y + CONT_H - (HDR_H - UiTheme.FONT_HEADING * 0.85f) / 2f, -17f); + panel.attachChild(t); + } + + // ── Button-Factory ──────────────────────────────────────────────────────── + + /** + * Erzeugt einen NinePatch-Button mit zentriertem Label und registriert ihn + * für automatische Klick-Erkennung. + */ + protected void addButton(Node parent, String label, + float x, float y, float w, float h, + BtnStyle style, Runnable action) { + NinePatch patch = switch (style) { + case SAVE -> NinePatch.buttonSave(assets); + case QUIT -> NinePatch.buttonQuit(assets); + case DISABLED -> NinePatch.buttonDisabled(assets); + case ARROW -> NinePatch.buttonArrow(assets); + default -> NinePatch.button(assets); + }; + parent.attachChild(patch.build(x, y, w, h, 0f)); + + ColorRGBA col = (style == BtnStyle.DISABLED) ? UiTheme.COL_MUTED : UiTheme.COL_WHITE; + BitmapText lbl = crispText(label, UiTheme.FONT_LABEL, col); + float tw = lbl.getLineWidth() / uiScale; + lbl.setLocalTranslation(x + (w - tw) / 2f, y + (h + UiTheme.FONT_LABEL * 0.85f) / 2f, 1f); + parent.attachChild(lbl); + + if (style != BtnStyle.DISABLED && action != null) { + buttons.add(new Btn(x, y, w, h, action)); + } + } + + // ── 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; + } + } + }; + + /** Hook für Subklassen die auch auf Klicks reagieren (z.B. ConfigScreen Zeilen). */ + protected void onScreenClick(float[] v) {} + + // ── Blur ────────────────────────────────────────────────────────────────── + + private void applyBlur() { + WorldScene ws = app.getStateManager().getState(WorldScene.class); + if (ws == null) return; + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp == null) return; + blurFilter = new GaussianBlurFilter(6f); + fpp.addFilter(blurFilter); + } + + private void removeBlur() { + if (blurFilter == null) return; + WorldScene ws = app.getStateManager().getState(WorldScene.class); + if (ws != null) { + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp != null) fpp.removeFilter(blurFilter); + } + blurFilter = null; + } + + // ── Shared Helpers ──────────────────────────────────────────────────────── + + /** Erzeugt BitmapText mit Counter-Scale-Technik für scharfes Rendering auf WQHD. */ + protected BitmapText crispText(String text, float virtualSize, ColorRGBA color) { + return UiTheme.crispText(font, text, virtualSize, color, uiScale); + } + + /** Bildschirmkoordinaten → virtuelle Canvas-Koordinaten. */ + protected float[] toVirtual(Vector2f screen) { + float scale = MenuCanvas.getScale(app.getCamera()); + float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f; + float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f; + return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale }; + } + + protected Geometry addQuad(Node parent, float x, float y, float w, float h, + ColorRGBA col, float z) { + Geometry g = new Geometry("q", new Quad(w, h)); + Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + m.setColor("Color", col.clone()); + m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + g.setQueueBucket(RenderQueue.Bucket.Gui); + g.setMaterial(m); + g.setLocalTranslation(x, y, z); + parent.attachChild(g); + return g; + } + + protected float getUiScale() { return uiScale; } +} 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 new file mode 100644 index 0000000..5beb6d9 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/NinePatch.java @@ -0,0 +1,324 @@ +package de.blight.game.config; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetKey; +import com.jme3.asset.AssetManager; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.shape.Quad; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.Set; + +/** + * Rendert eine Textur als 9-Patch: Ecken bleiben unverzerrt, + * Kanten strecken in einer Achse, Mitte streckt in beiden. + * + * Borders werden automatisch aus Guide-Pixeln erkannt (Alpha=35 oder 140). + * Die Guide-Pixel werden beim Laden durch die benachbarte Rahmenfarbe ersetzt, + * sodass sie im Rendering vollständig unsichtbar sind. + * + * Verwendung: + * Node n = NinePatch.panel(assets).build(px, py, pw, ph, -1f); + * parentNode.attachChild(n); + */ +public class NinePatch { + + private static final Set GUIDE_ALPHAS = Set.of(35, 140); + + // ── Vorkonfigurierte Factories ────────────────────────────────────────── + + public static NinePatch container(AssetManager assets) { + return fromPng(assets, "Textures/menu/container.png"); + } + + public static NinePatch panel(AssetManager assets) { + return fromPng(assets, "Textures/menu/panel.png"); + } + + public static NinePatch button(AssetManager assets) { + return fromPng(assets, "Textures/menu/button.png"); + } + + public static NinePatch buttonSave(AssetManager assets) { + return fromPng(assets, "Textures/menu/button_save.png"); + } + + public static NinePatch buttonQuit(AssetManager assets) { + return fromPng(assets, "Textures/menu/button_quit.png"); + } + + public static NinePatch buttonArrow(AssetManager assets) { + return fromPng(assets, "Textures/menu/button_arrow.png"); + } + + public static NinePatch buttonDisabled(AssetManager assets) { + return fromPng(assets, "Textures/menu/button_disabled.png"); + } + + public static NinePatch tabActive(AssetManager assets) { + return fromPng(assets, "Textures/menu/tab_active.png"); + } + + public static NinePatch tabInactive(AssetManager assets) { + return fromPng(assets, "Textures/menu/tab_inactive.png"); + } + + public static NinePatch itemCell(AssetManager assets) { + return fromPng(assets, "Textures/menu/item_cell.png"); + } + + // Scrollbars haben keine Guide-Pixel → explizite Werte + normales Laden + public static NinePatch scrollbarTrack(AssetManager assets) { + Texture tex = assets.loadTexture("Textures/menu/scrollbar_track.png"); + return new NinePatch(assets, tex, 16, 32, 4, 4, 4, 4); + } + + public static NinePatch scrollbarThumb(AssetManager assets) { + Texture tex = assets.loadTexture("Textures/menu/scrollbar_thumb.png"); + return new NinePatch(assets, tex, 16, 32, 4, 4, 4, 4); + } + + // ── Auto-Detection ───────────────────────────────────────────────────── + + /** + * Lädt PNG, erkennt Borders aus Guide-Pixeln, ersetzt die Guide-Pixel + * durch die benachbarte Rahmenfarbe und erzeugt eine aufbereitete Textur. + */ + private static NinePatch fromPng(AssetManager assets, String path) { + AssetInfo info = assets.locateAsset(new AssetKey<>(path)); + if (info == null) { + throw new RuntimeException("NinePatch-Asset nicht gefunden: " + path); + } + BufferedImage img; + try (InputStream is = info.openStream()) { + img = ImageIO.read(is); + if (img == null) { + throw new RuntimeException("ImageIO.read() lieferte null für: " + path); + } + } catch (IOException e) { + throw new RuntimeException("NinePatch-Ladefehler: " + path, e); + } + + // Sicherstellen, dass wir ARGB-Zugriff haben + if (img.getType() != BufferedImage.TYPE_INT_ARGB) { + BufferedImage argb = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); + argb.getGraphics().drawImage(img, 0, 0, null); + img = argb; + } + + int w = img.getWidth(); + int h = img.getHeight(); + int[] borders = detectBorders(img, w, h); + int l = borders[0], r = borders[1], t = borders[2], b = borders[3]; + + // Guide-Pixel durch Rahmen-Nachbarfarbe ersetzen + stripGuides(img, w, h, l - 1, w - r, t - 1, h - b); + + Texture2D tex = toJmeTexture(img, w, h); + return new NinePatch(assets, tex, w, h, l, r, t, b); + } + + /** + * Erkennt Border-Werte anhand der Guide-Positionen. + * + * Mittlere Zeile → linke/rechte Guide-X-Position + * Mittlere Spalte → obere/untere Guide-Y-Position (PNG-Koordinaten) + * + * Formel: linke/obere Border = guidePos + 1 (Guide im fixen Strip, kein Stretch). + * rechte/untere Border = texDim - guidePos (Guide ist erstes Pixel des Strips). + */ + private static int[] detectBorders(BufferedImage img, int w, int h) { + int midX = w / 2; + int midY = h / 2; + + int lx = -1, rx = -1; + for (int x = 0; x < w; x++) { + int a = (img.getRGB(x, midY) >> 24) & 0xff; + if (GUIDE_ALPHAS.contains(a)) { + if (lx < 0) { lx = x; } + rx = x; + } + } + + int ty = -1, by = -1; + for (int y = 0; y < h; y++) { + int a = (img.getRGB(midX, y) >> 24) & 0xff; + if (GUIDE_ALPHAS.contains(a)) { + if (ty < 0) { ty = y; } + by = y; + } + } + + int l = lx >= 0 ? lx + 1 : w / 4; + int r = rx >= 0 ? w - rx : w / 4; + int t = ty >= 0 ? ty + 1 : h / 4; + int b = by >= 0 ? h - by : h / 4; + + return new int[]{l, r, t, b}; + } + + /** + * Ersetzt Guide-Pixel durch die Farbe des nächsten Nicht-Guide-Pixels + * in Richtung des Rahmen-Rands (weg von der Bildmitte). + * + * lx, rx: Spaltenindizes der linken/rechten Guide-Linie + * 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) + 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); + } + } + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + if (!isGuide[y][x]) { continue; } + + // Richtung: weg von der Bildmitte, in Richtung des Rahmens + boolean col = (x == lx || x == rx); + boolean row = (y == ty || y == by); + + int dx = col ? (x == lx ? -1 : +1) : 0; + int dy = row ? (y == ty ? -1 : +1) : 0; + + if (dx == 0 && dy == 0) { + // Verirrter Guide-Pixel außerhalb der bekannten Linien + img.setRGB(x, y, 0); + continue; + } + + img.setRGB(x, y, walkToNonGuide(img, isGuide, x + dx, y + dy, dx, dy, w, h)); + } + } + } + + private static int walkToNonGuide(BufferedImage img, boolean[][] isGuide, + int x, int y, int dx, int dy, int w, int h) { + while (x >= 0 && x < w && y >= 0 && y < h) { + if (!isGuide[y][x]) { return img.getRGB(x, y); } + x += dx; + y += dy; + } + return 0; // Fallback: transparent + } + + /** + * Konvertiert ein BufferedImage in eine JME3-Textur. + * Zeilen werden von unten nach oben gelesen (entspricht JME3-Y-Flip beim PNG-Laden). + */ + private static Texture2D toJmeTexture(BufferedImage img, int w, int h) { + ByteBuffer buf = ByteBuffer.allocateDirect(w * h * 4); + int[] row = new int[w]; + for (int y = h - 1; y >= 0; y--) { + img.getRGB(0, y, w, 1, row, 0, w); + for (int argb : row) { + buf.put((byte) ((argb >> 16) & 0xff)); // R + buf.put((byte) ((argb >> 8) & 0xff)); // G + buf.put((byte) ( argb & 0xff)); // B + buf.put((byte) ((argb >> 24) & 0xff)); // A + } + } + buf.flip(); + com.jme3.texture.Image jmeImg = new com.jme3.texture.Image( + com.jme3.texture.Image.Format.RGBA8, w, h, buf, ColorSpace.sRGB); + return new Texture2D(jmeImg); + } + + // ── Instanz ──────────────────────────────────────────────────────────── + + private final AssetManager assets; + private final Texture tex; + private final float texW, texH; + private final float l, r, t, b; + + public NinePatch(AssetManager assets, Texture tex, + int texW, int texH, int l, int r, int t, int b) { + this.assets = assets; + this.tex = tex; + this.texW = texW; + this.texH = texH; + this.l = l; + this.r = r; + this.t = t; + this.b = b; + } + + /** + * Baut den Node mit 9 Quads. x/y sind Bildschirm-Koordinaten + * (Gui-Space, y=0 unten). z bestimmt die Render-Reihenfolge. + */ + public Node build(float x, float y, float w, float h, float z) { + tex.setWrap(Texture.WrapMode.EdgeClamp); + tex.setMagFilter(Texture.MagFilter.Nearest); + + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setTexture("ColorMap", tex); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + + // UV-Koordinaten (V=0 = Textur-Unterkante in JME3) + float u1 = l / texW; + float u2 = (texW - r) / texW; + float v1 = b / texH; // unterer Rand in UV + float v2 = (texH - t) / texH; // oberer Rand in UV + + // Bildschirm-Positionen der Nahtlinien + float sx1 = x + l, sx2 = x + w - r; + float sy1 = y + b, sy2 = y + h - t; + + 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)); + return n; + } + + private static Geometry q(Material mat, + float x, float y, float w, float h, + float uMin, float vMin, float uMax, float vMax, + float z) { + if (w <= 0 || h <= 0) { return new Geometry(); } + Quad mesh = new Quad(w, h); + + // UV-Buffer überschreiben (JME3-Quad Vertex-Reihenfolge: BL, BR, TR, TL) + FloatBuffer uvBuf = mesh.getFloatBuffer(VertexBuffer.Type.TexCoord); + uvBuf.rewind(); + uvBuf.put(uMin).put(vMin) // BL + .put(uMax).put(vMin) // BR + .put(uMax).put(vMax) // TR + .put(uMin).put(vMax); // TL + uvBuf.rewind(); + mesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded(); + + Geometry g = new Geometry("np", mesh); + g.setMaterial(mat); + g.setQueueBucket(RenderQueue.Bucket.Gui); + g.setLocalTranslation(x, y, z); + return g; + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/OverlayState.java b/blight-game/src/main/java/de/blight/game/config/OverlayState.java new file mode 100644 index 0000000..6674a61 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/OverlayState.java @@ -0,0 +1,247 @@ +package de.blight.game.config; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.post.FilterPostProcessor; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; +import de.blight.game.post.GaussianBlurFilter; +import de.blight.game.scene.WorldScene; + +/** + * Abstrakte Basisklasse für alle Overlay-Screens (Inventar, Charakter, Quest). + * + * Kümmert sich um: + * - Mutual exclusion: nur ein Overlay gleichzeitig aktiv + * - 90%-Bildschirm-Panel mit dunklem Header + * - bgLayer + canvasNode Aufbau/Abbau + * - Blur-Filter-Verwaltung + * - Welt-Pause/Resume + * - ESC-Schließen + * - Crisp-Text-Helper (Counter-Scale für scharfe Schrift) + * - toVirtual-Helper (Screen → virtuelle Canvas-Koordinaten) + */ +public abstract class OverlayState extends BaseAppState { + + // ── Mutual Exclusion ────────────────────────────────────────────────────── + + private static OverlayState activeOverlay; + + // ── Panel-Dimensionen (90% des Referenzraums) ───────────────────────────── + + public static final float PW = MenuCanvas.CONT_W; + public static final float PH = MenuCanvas.CONT_H; + public static final float PX = MenuCanvas.CONT_X; + public static final float PY = MenuCanvas.CONT_Y; + public static final float HDR_H = 44f; + public static final float PAD = 16f; + + // Content-Bereich (innerhalb Header und Padding) + public static final float CX = PX + PAD; + public static final float CY = PY + PAD; + public static final float CW = PW - PAD * 2f; + public static final float CH = PH - HDR_H - PAD * 2f; + + // ── Input-Mapping für ESC (pro-Instanz eindeutiger Name) ────────────────── + + private final String MAP_ESC = "_OverlayEsc_" + System.identityHashCode(this); + + // ── Zustand, den Unterklassen nutzen ────────────────────────────────────── + + protected SimpleApplication app; + protected AssetManager assets; + protected BitmapFont font; + protected Node guiNode; + protected Node canvasNode; + + // ── Interner Zustand ────────────────────────────────────────────────────── + + private Node bgLayer; + private GaussianBlurFilter blurFilter; + private float uiScale; + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + protected void initialize(Application application) { + app = (SimpleApplication) application; + assets = app.getAssetManager(); + guiNode = app.getGuiNode(); + font = assets.loadFont("Interface/Fonts/Default.fnt"); + + app.getInputManager().addMapping(MAP_ESC, new KeyTrigger(KeyInput.KEY_ESCAPE)); + app.getInputManager().addListener(escListener, MAP_ESC); + + initOverlayKeys(); + } + + @Override + protected void onEnable() { + // Mutual exclusion: aktives Overlay schließen + if (activeOverlay != null && activeOverlay != this) { + activeOverlay.setEnabled(false); + } + activeOverlay = this; + + // Canvas + bgLayer aufbauen + uiScale = MenuCanvas.getScale(app.getCamera()); + bgLayer = MenuCanvas.createBgLayer(assets, app.getCamera()); + canvasNode = MenuCanvas.createCanvas(app.getCamera()); + guiNode.attachChild(bgLayer); + guiNode.attachChild(canvasNode); + + // Äußerer Container: NinePatch-Rahmen (Hintergrund + Border) → Header + Node panel = new Node("overlay-panel"); + panel.attachChild(NinePatch.container(assets).build(PX, PY, PW, PH, -19f)); + + // Header-Balken + addQuad(panel, PX, PY + PH - HDR_H, PW, HDR_H, UiTheme.COL_HDR, -18f); + + // Titel + BitmapText titleTxt = crispText(getTitle(), UiTheme.FONT_TITLE, UiTheme.COL_WHITE); + titleTxt.setLocalTranslation(PX + PAD, PY + PH - PAD / 2f, -17f); + panel.attachChild(titleTxt); + + // Inhalt vom Subtyp aufbauen + buildContent(panel, uiScale); + + canvasNode.attachChild(panel); + + // Blur + Welt pausieren + Cursor sichtbar + WorldScene ws = app.getStateManager().getState(WorldScene.class); + if (ws != null) { + ws.setPaused(true); + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp != null) { + blurFilter = new GaussianBlurFilter(6f); + fpp.addFilter(blurFilter); + } + } + app.getInputManager().setCursorVisible(true); + } + + @Override + protected void onDisable() { + if (activeOverlay == this) { + activeOverlay = null; + } + + // Canvas + bgLayer abbauen + if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; } + if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } + + // Blur entfernen + Welt resumieren + Cursor ausblenden + WorldScene ws = app.getStateManager().getState(WorldScene.class); + if (ws != null) { + ws.setPaused(false); + if (blurFilter != null) { + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp != null) { fpp.removeFilter(blurFilter); } + blurFilter = null; + } + } + app.getInputManager().setCursorVisible(false); + + onOverlayClosed(); + } + + @Override + protected void cleanup(Application application) { + app.getInputManager().deleteMapping(MAP_ESC); + cleanupOverlayKeys(); + } + + // ── ESC-Listener ───────────────────────────────────────────────────────── + + private final ActionListener escListener = (name, pressed, tpf) -> { + if (pressed && isEnabled()) { + setEnabled(false); + } + }; + + // ── Abstrakte Methoden für Unterklassen ─────────────────────────────────── + + /** Überschriftstext im Header-Balken. */ + protected abstract String getTitle(); + + /** + * Inhalt aufbauen. Der panel-Node ist bereits mit NinePatch + Header versehen. + * Unterklassen fügen hier ihre Widgets ein. + * + * @param panel Empfänger-Node (bereits an canvasNode gehängt) + * @param scale Canvas-Skalierungsfaktor (für crispText) + */ + protected abstract void buildContent(Node panel, float scale); + + /** + * Toggle-Taste und weitere Eingaben in initialize() registrieren. + * Wird von {@link #initialize} aufgerufen. + */ + protected abstract void initOverlayKeys(); + + /** + * Toggle-Taste und weitere Eingaben in cleanup() deregistrieren. + * Wird von {@link #cleanup} aufgerufen. + */ + protected abstract void cleanupOverlayKeys(); + + // ── Optionaler Hook ─────────────────────────────────────────────────────── + + /** Wird nach onDisable() aufgerufen – für subklassenspezifisches Teardown. */ + protected void onOverlayClosed() {} + + // ── Shared Helpers ──────────────────────────────────────────────────────── + + /** + * Erstellt einen BitmapText mit Counter-Scale-Technik: + * - Schrift wird mit virtualSize*uiScale Pixeln gerendert (scharf) + * - LocalScale 1/uiScale bringt ihn zurück auf virtualSize virtuelle Einheiten + */ + protected BitmapText crispText(String text, float virtualSize, ColorRGBA color) { + return UiTheme.crispText(font, text, virtualSize, color, uiScale); + } + + /** + * Übersetzt Bildschirmkoordinaten (JME3 Pixel) in virtuelle Canvas-Koordinaten. + */ + protected float[] toVirtual(Vector2f screen) { + float scale = MenuCanvas.getScale(app.getCamera()); + float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f; + float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f; + return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale }; + } + + /** + * Erzeugt ein farbiges Quad, hängt es an parent und gibt die Geometry zurück. + */ + protected Geometry addQuad(Node parent, float x, float y, float w, float h, + ColorRGBA col, float z) { + Geometry g = new Geometry("q", new Quad(w, h)); + Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + m.setColor("Color", col.clone()); + m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + g.setQueueBucket(RenderQueue.Bucket.Gui); + g.setMaterial(m); + g.setLocalTranslation(x, y, z); + parent.attachChild(g); + return g; + } + + /** Aktueller uiScale-Wert (gesetzt beim letzten onEnable). */ + protected float getUiScale() { + return uiScale; + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/UiTheme.java b/blight-game/src/main/java/de/blight/game/config/UiTheme.java new file mode 100644 index 0000000..dd1e88e --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/config/UiTheme.java @@ -0,0 +1,228 @@ +package de.blight.game.config; + +import com.jme3.asset.AssetManager; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; + +/** + * Zentrale Theme-Konstanten und Widget-Factories für alle Overlay-Screens. + * Keine Instanzierung – alles statisch. + */ +public final class UiTheme { + + private UiTheme() {} + + // ── Farben ──────────────────────────────────────────────────────────────── + + public static final ColorRGBA COL_PANEL = new ColorRGBA(0.20f, 0.20f, 0.32f, 0.97f); + public static final ColorRGBA COL_HDR = new ColorRGBA(0.10f, 0.10f, 0.20f, 1.00f); + public static final ColorRGBA COL_TAB_ON = new ColorRGBA(0.22f, 0.22f, 0.42f, 1.00f); + public static final ColorRGBA COL_TAB_OFF = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f); + public static final ColorRGBA COL_WHITE = ColorRGBA.White.clone(); + public static final ColorRGBA COL_MUTED = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f); + public static final ColorRGBA COL_GOLD = new ColorRGBA(1.00f, 0.85f, 0.20f, 1.00f); + public static final ColorRGBA COL_CELL = new ColorRGBA(0.14f, 0.14f, 0.22f, 1.00f); + public static final ColorRGBA COL_CELL_FRAME = new ColorRGBA(0.22f, 0.22f, 0.35f, 1.00f); + public static final ColorRGBA COL_DIVIDER = new ColorRGBA(0.22f, 0.22f, 0.35f, 0.80f); + public static final ColorRGBA COL_SUBHDR = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.80f); + public static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.20f, 0.20f, 0.28f, 1.00f); + public static final ColorRGBA COL_BADGE = new ColorRGBA(0.04f, 0.04f, 0.07f, 0.92f); + public static final ColorRGBA COL_SUBCAT_TXT = new ColorRGBA(0.55f, 0.78f, 1.00f, 1.00f); + + // Character-State-Farben + public static final ColorRGBA COL_HP = new ColorRGBA(0.85f, 0.10f, 0.10f, 1.00f); + public static final ColorRGBA COL_STAMINA = new ColorRGBA(0.90f, 0.80f, 0.10f, 1.00f); + public static final ColorRGBA COL_MANA = new ColorRGBA(0.10f, 0.35f, 0.90f, 1.00f); + public static final ColorRGBA COL_BAR_BG = new ColorRGBA(0.10f, 0.10f, 0.14f, 1.00f); + public static final ColorRGBA COL_SECTION = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.90f); + public static final ColorRGBA COL_CARD = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f); + public static final ColorRGBA COL_CARD_BRD = new ColorRGBA(0.22f, 0.22f, 0.35f, 1.00f); + public static final ColorRGBA COL_UNLOCKED = new ColorRGBA(0.40f, 0.90f, 0.40f, 1.00f); + public static final ColorRGBA COL_LOCKED = new ColorRGBA(0.35f, 0.35f, 0.38f, 1.00f); + public static final ColorRGBA COL_PIP_ON = COL_GOLD; + public static final ColorRGBA COL_PIP_OFF = new ColorRGBA(0.22f, 0.22f, 0.28f, 1.00f); + public static final ColorRGBA COL_XP_BAR = new ColorRGBA(0.30f, 0.65f, 1.00f, 1.00f); + + // Quest-State-Farben + public static final ColorRGBA COL_SELECTED = new ColorRGBA(0.20f, 0.20f, 0.38f, 1.00f); + public static final ColorRGBA COL_ITEM_BG = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f); + public static final ColorRGBA COL_DETAIL_BG = new ColorRGBA(0.08f, 0.08f, 0.14f, 1.00f); + public static final ColorRGBA COL_DIALOG_BG = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.90f); + public static final ColorRGBA COL_XP = new ColorRGBA(0.40f, 0.90f, 0.40f, 1.00f); + public static final ColorRGBA COL_SUCCESS = new ColorRGBA(0.30f, 0.80f, 0.30f, 1.00f); + public static final ColorRGBA COL_COMPLETED = new ColorRGBA(0.40f, 0.65f, 0.40f, 1.00f); + public static final ColorRGBA COL_TYPE_TALK = new ColorRGBA(0.40f, 0.75f, 1.00f, 1.00f); + public static final ColorRGBA COL_TYPE_ITEM = new ColorRGBA(1.00f, 0.75f, 0.25f, 1.00f); + public static final ColorRGBA COL_TYPE_BRING = new ColorRGBA(0.85f, 0.45f, 1.00f, 1.00f); + public static final ColorRGBA COL_TYPE_FOLLOW = new ColorRGBA(0.45f, 1.00f, 0.85f, 1.00f); + public static final ColorRGBA COL_TYPE_INTERACT= new ColorRGBA(1.00f, 0.55f, 0.40f, 1.00f); + + // Checkbox + public static final ColorRGBA COL_CHECK_BOX = new ColorRGBA(0.15f, 0.15f, 0.25f, 1.00f); + public static final ColorRGBA COL_CHECK_BORDER = new ColorRGBA(0.30f, 0.30f, 0.50f, 1.00f); + public static final ColorRGBA COL_CHECK_TICK = new ColorRGBA(0.40f, 0.90f, 0.40f, 1.00f); + + // ComboBox + public static final ColorRGBA COL_COMBO_BG = new ColorRGBA(0.12f, 0.12f, 0.22f, 1.00f); + public static final ColorRGBA COL_COMBO_BORDER = new ColorRGBA(0.28f, 0.28f, 0.45f, 1.00f); + public static final ColorRGBA COL_COMBO_ARROW = new ColorRGBA(0.70f, 0.70f, 0.80f, 1.00f); + + // ── Schriftgrößen (virtuelle Pixel) ─────────────────────────────────────── + + public static final float FONT_TITLE = 20f; + public static final float FONT_HEADING = 16f; + public static final float FONT_LABEL = 14f; + public static final float FONT_BODY = 12f; + public static final float FONT_SMALL = 11f; + public static final float FONT_TINY = 10f; + + // ── Widget: Checkbox ────────────────────────────────────────────────────── + + /** + * Erzeugt eine Checkbox-Node in virtuellen Canvas-Koordinaten. + * Die Node-UserData "bounds" enthält float[]{x, y, 20, 20} für Hit-Testing. + * + * @param assets AssetManager + * @param font BitmapFont für das Label + * @param x linke Kante in virtuellen Koordinaten + * @param y untere Kante in virtuellen Koordinaten (JME3 Y-up) + * @param label Beschriftung rechts der Box + * @param checked initialer Zustand + * @param scale Canvas-Skalierungsfaktor (von MenuCanvas.getScale) + * @return Node, der an den Panel-Node gehängt werden kann + */ + public static Node checkbox(AssetManager assets, BitmapFont font, + float x, float y, String label, + boolean checked, float scale) { + final float BOX_SZ = 20f; + Node n = new Node("checkbox"); + + // Rahmen + Geometry border = quad(assets, x, y, BOX_SZ, BOX_SZ, COL_CHECK_BORDER, -15f); + n.attachChild(border); + // Innen + Geometry inner = quad(assets, x + 1, y + 1, BOX_SZ - 2, BOX_SZ - 2, COL_CHECK_BOX, -14f); + n.attachChild(inner); + + // Häkchen (zwei Quads als Kreuzbalken) + if (checked) { + // Diagonale von unten-links nach oben-rechts + float thick = 3f; + Geometry tick1 = quad(assets, x + 3, y + 7, BOX_SZ - 6, thick, COL_CHECK_TICK, -13f); + tick1.rotate(0f, 0f, (float) Math.toRadians(45)); + n.attachChild(tick1); + Geometry tick2 = quad(assets, x + 3, y + 7, BOX_SZ - 6, thick, COL_CHECK_TICK, -13f); + tick2.rotate(0f, 0f, (float) Math.toRadians(-45)); + n.attachChild(tick2); + } + + // Label rechts der Box + if (label != null && !label.isEmpty()) { + BitmapText lbl = crispText(font, label, FONT_BODY, COL_WHITE, scale); + lbl.setLocalTranslation(x + BOX_SZ + 6, y + BOX_SZ - 2, -14f); + n.attachChild(lbl); + } + + // Bounds für Hit-Testing speichern + n.setUserData("bounds", new float[]{x, y, BOX_SZ, BOX_SZ}); + return n; + } + + // ── Widget: ComboBox ────────────────────────────────────────────────────── + + /** + * Erzeugt eine einfache ComboBox-Node in virtuellen Canvas-Koordinaten. + * Zeigt den ausgewählten Eintrag + Pfeil nach unten. + * Die Node-UserData "bounds" enthält float[]{x, y, w, 24} für Hit-Testing. + * + * @param assets AssetManager + * @param font BitmapFont + * @param x linke Kante + * @param y untere Kante + * @param w Breite + * @param options alle Einträge + * @param selectedIndex ausgewählter Index + * @param scale Canvas-Skalierungsfaktor + * @return Node + */ + public static Node comboBox(AssetManager assets, BitmapFont font, + float x, float y, float w, + String[] options, int selectedIndex, + float scale) { + final float H = 24f; + Node n = new Node("combobox"); + + // Hintergrund + Geometry bg = quad(assets, x, y, w, H, COL_COMBO_BG, -15f); + n.attachChild(bg); + // Rahmen + Geometry border = quad(assets, x, y, w, 1f, COL_COMBO_BORDER, -14f); + n.attachChild(border); + Geometry borderT = quad(assets, x, y + H - 1, w, 1f, COL_COMBO_BORDER, -14f); + n.attachChild(borderT); + Geometry borderL = quad(assets, x, y, 1f, H, COL_COMBO_BORDER, -14f); + n.attachChild(borderL); + Geometry borderR = quad(assets, x + w - 1, y, 1f, H, COL_COMBO_BORDER, -14f); + n.attachChild(borderR); + + // Pfeil rechts + final float ARR_W = 20f; + Geometry arrBg = quad(assets, x + w - ARR_W, y, ARR_W, H, COL_COMBO_BORDER, -14f); + n.attachChild(arrBg); + BitmapText arrow = crispText(font, "v", FONT_TINY, COL_COMBO_ARROW, scale); + arrow.setLocalTranslation(x + w - ARR_W + (ARR_W - arrow.getLineWidth()) / 2f, + y + H - (H - arrow.getLineHeight()) / 2f, -13f); + n.attachChild(arrow); + + // Ausgewählter Text + String selText = (options != null && selectedIndex >= 0 && selectedIndex < options.length) + ? options[selectedIndex] : ""; + BitmapText selLbl = crispText(font, selText, FONT_BODY, COL_WHITE, scale); + selLbl.setLocalTranslation(x + 6, y + H - (H - selLbl.getLineHeight()) / 2f, -13f); + n.attachChild(selLbl); + + n.setUserData("bounds", new float[]{x, y, w, H}); + return n; + } + + // ── Interne Hilfsmethoden ───────────────────────────────────────────────── + + /** + * Erzeugt einen BitmapText mit Counter-Scale-Technik für scharfes Rendering. + * Der Text wird mit virtualSize*scale Pixeln gerendert (scharf), aber erscheint + * in virtualSize virtuellen Einheiten auf dem Canvas. + */ + public static BitmapText crispText(BitmapFont font, String text, + float virtualSize, ColorRGBA color, float scale) { + BitmapText t = new BitmapText(font); + t.setSize(virtualSize * scale); + t.setColor(color.clone()); + t.setText(text); + t.setLocalScale(1f / scale, 1f / scale, 1f); + return t; + } + + private static Geometry quad(AssetManager assets, + float x, float y, float w, float h, + ColorRGBA col, float z) { + Geometry g = new Geometry("q", new Quad(w, h)); + Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + m.setColor("Color", col.clone()); + if (col.a < 1f) { + m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + g.setQueueBucket(RenderQueue.Bucket.Transparent); + } else { + g.setQueueBucket(RenderQueue.Bucket.Gui); + } + g.setMaterial(m); + g.setLocalTranslation(x, y, z); + return g; + } +}