gitignore-Fix: Quell-Paket config/ nicht mehr ignorieren

**/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 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:26:56 +02:00
parent 944b3fae34
commit 2c88b7e313
10 changed files with 1407 additions and 1 deletions

View File

@@ -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); }
}

View File

@@ -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;
}

View File

@@ -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());
}
}
}

View File

@@ -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); }
}

View File

@@ -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;
}
}

View File

@@ -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<Btn> 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; }
}

View File

@@ -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<Integer> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}