Weiter am Menü und an der Vollbildproblematik gearbeitet
This commit is contained in:
@@ -18,7 +18,14 @@ import de.blight.game.console.JmeConsole;
|
||||
import de.blight.game.scene.WorldScene;
|
||||
import de.blight.lang.TextResolver;
|
||||
|
||||
import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.glfw.GLFWVidMode;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import com.jme3.system.lwjgl.LwjglWindow;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Locale;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
@@ -47,6 +54,11 @@ public class BlightGame extends SimpleApplication {
|
||||
/** Routed durch onClose-Callbacks, damit Config/Grafik-Screen zurück zum richtigen Menü springen. */
|
||||
private Runnable screenCloseTarget;
|
||||
|
||||
// Vollbild-Watchdog: erkennt, wenn das Fenster unerwartet die Auflösung wechselt
|
||||
// (z.B. GNOME Shell de-fullscreent OverrideRedirect-Fenster bei Monitorfokus-Wechsel)
|
||||
private long startTimeMs = 0;
|
||||
private long lastFullscreenFix = 0;
|
||||
|
||||
private JWindow splashWindow;
|
||||
|
||||
// ── Splash-Status (von JME-Thread geschrieben, EDT liest) ─────────────────
|
||||
@@ -69,11 +81,41 @@ public class BlightGame extends SimpleApplication {
|
||||
try {
|
||||
settings.setIcons(new Object[]{ImageIO.read(BlightGame.class.getResourceAsStream("/icon.png"))});
|
||||
} catch (IOException | NullPointerException ignored) {}
|
||||
settings.setResolution(gs.width, gs.height);
|
||||
int targetW = gs.width, targetH = gs.height;
|
||||
if (gs.fullscreen) {
|
||||
// GNOME Shell lehnt Nicht-Nativ-Auflösungen bei GLFW-Vollbild (OverrideRedirect) ab.
|
||||
// Nicht-native Auflösungen führen zu WM-Dekorationen oder falschem Monitor.
|
||||
// → Immer native Auflösung des AWT-Primärmonitors verwenden.
|
||||
try {
|
||||
Rectangle screen = GraphicsEnvironment
|
||||
.getLocalGraphicsEnvironment()
|
||||
.getDefaultScreenDevice()
|
||||
.getDefaultConfiguration()
|
||||
.getBounds();
|
||||
targetW = screen.width;
|
||||
targetH = screen.height;
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
settings.setResolution(targetW, targetH);
|
||||
settings.setFullscreen(gs.fullscreen);
|
||||
settings.setBitsPerPixel(32);
|
||||
settings.setVSync(gs.vsync);
|
||||
settings.setSamples(gs.samples);
|
||||
// X11 erzwingen: auf Systemen mit Wayland-Libs läuft GLFW sonst im Wayland-Modus,
|
||||
// was libdecor-Fensterrahmen auch im Vollbild aktiviert.
|
||||
settings.setX11PlatformPreferred(true);
|
||||
|
||||
// Splash vor GLFW-Fenstererstellung schließen: verhindert auto-iconify
|
||||
// (JWindow kann X11-Fokus zurückbekommen → GLFW-Vollbild sofort minimiert)
|
||||
if (gs.fullscreen && app.splashWindow != null) {
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
app.splashWindow.setVisible(false);
|
||||
app.splashWindow.dispose();
|
||||
});
|
||||
} catch (Exception ignored) {}
|
||||
app.splashWindow = null;
|
||||
}
|
||||
|
||||
status("Initialisiere Renderer...");
|
||||
app.setSettings(settings);
|
||||
@@ -143,6 +185,16 @@ public class BlightGame extends SimpleApplication {
|
||||
keyBindings = KeyBindingStore.load();
|
||||
graphicsSettings = GraphicsStore.load();
|
||||
audioSettings = AudioSettingsStore.load();
|
||||
|
||||
startTimeMs = System.currentTimeMillis();
|
||||
if (graphicsSettings.fullscreen) {
|
||||
// Tatsächliche Cam-Auflösung als Watchdog-Baseline (main() nutzt native Monitorgröße)
|
||||
graphicsSettings.width = cam.getWidth();
|
||||
graphicsSettings.height = cam.getHeight();
|
||||
log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={}",
|
||||
cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen);
|
||||
fixFullscreenMonitor();
|
||||
}
|
||||
stateManager.attach(new AudioSettingsState(audioSettings));
|
||||
|
||||
status("Lade Spielstand...");
|
||||
@@ -340,6 +392,21 @@ public class BlightGame extends SimpleApplication {
|
||||
gameReady = true;
|
||||
status("Bereit");
|
||||
}
|
||||
// Vollbild-Watchdog: Wenn GNOME Shell oder der WM das Fenster unerwartet aus dem
|
||||
// Vollbild-Modus holt (erkennbar an falscher Auflösung), sofort wiederherstellen.
|
||||
// 5s Anlaufzeit überspringen (Initialisierungs-Resize-Events), 3s Cooldown.
|
||||
if (graphicsSettings != null && graphicsSettings.fullscreen) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - startTimeMs > 5_000
|
||||
&& (cam.getWidth() != graphicsSettings.width
|
||||
|| cam.getHeight() != graphicsSettings.height)
|
||||
&& now - lastFullscreenFix > 3_000) {
|
||||
log.warn("[Grafik] Vollbild verloren ({}×{} statt {}×{}), starte Wiederherstellung",
|
||||
cam.getWidth(), cam.getHeight(), graphicsSettings.width, graphicsSettings.height);
|
||||
lastFullscreenFix = now;
|
||||
fixFullscreenMonitor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Konsolen-Befehle ─────────────────────────────────────────────────────
|
||||
@@ -388,7 +455,7 @@ public class BlightGame extends SimpleApplication {
|
||||
}
|
||||
});
|
||||
|
||||
console.registerCommand("weather", args -> {
|
||||
console.registerCommand("weather", (args) -> {
|
||||
de.blight.game.state.WeatherState ws =
|
||||
stateManager.getState(de.blight.game.state.WeatherState.class);
|
||||
if (ws == null) return "Wettersystem nicht aktiv";
|
||||
@@ -405,4 +472,81 @@ public class BlightGame extends SimpleApplication {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Vollbild sicherstellen: GLFW ist hier bereits durch JME3 initialisiert.
|
||||
// Problem: GLFW_AUTO_ICONIFY (default true) minimiert das Vollbild-Fenster sofort,
|
||||
// wenn beim Start ein anderes Fenster (z.B. Editor) noch den Fokus hat.
|
||||
// Der Editor minimiert sich inzwischen selbst vor dem Spielstart – dieser Fix greift
|
||||
// dennoch als Fallback und korrigiert außerdem einen falschen Monitor-Index.
|
||||
private void fixFullscreenMonitor() {
|
||||
if (!(context instanceof LwjglWindow)) {
|
||||
log.warn("[Grafik] Kein LwjglWindow-Kontext, Vollbild-Fix übersprungen");
|
||||
return;
|
||||
}
|
||||
long win = ((LwjglWindow) context).getWindowHandle();
|
||||
if (win == 0L) {
|
||||
log.warn("[Grafik] Window-Handle ist 0, Vollbild-Fix übersprungen");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Auto-Iconify deaktivieren
|
||||
GLFW.glfwSetWindowAttrib(win, GLFW.GLFW_AUTO_ICONIFY, GLFW.GLFW_FALSE);
|
||||
// Ggf. iconifiziertes Fenster wiederherstellen
|
||||
GLFW.glfwRestoreWindow(win);
|
||||
|
||||
// Ist das Fenster bereits in GLFW-Vollbild (OverrideRedirect)?
|
||||
long currentMonitor = GLFW.glfwGetWindowMonitor(win);
|
||||
|
||||
// AWT-Primary-Monitor mit GLFW-Monitoren abgleichen
|
||||
Rectangle awt = GraphicsEnvironment
|
||||
.getLocalGraphicsEnvironment()
|
||||
.getDefaultScreenDevice()
|
||||
.getDefaultConfiguration()
|
||||
.getBounds();
|
||||
|
||||
PointerBuffer mons = GLFW.glfwGetMonitors();
|
||||
if (mons == null || mons.limit() == 0) {
|
||||
log.warn("[Grafik] glfwGetMonitors() liefert nichts");
|
||||
GLFW.glfwFocusWindow(win);
|
||||
return;
|
||||
}
|
||||
|
||||
long target = GLFW.glfwGetPrimaryMonitor();
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
IntBuffer mx = stack.mallocInt(1);
|
||||
IntBuffer my = stack.mallocInt(1);
|
||||
for (int i = 0; i < mons.limit(); i++) {
|
||||
GLFW.glfwGetMonitorPos(mons.get(i), mx, my);
|
||||
log.info("[Grafik] GLFW-Monitor {}: pos=({},{}) | AWT-Primary: ({},{}) | aktuellerMonitor={}",
|
||||
i, mx.get(0), my.get(0), awt.x, awt.y,
|
||||
(currentMonitor == mons.get(i)) ? "JA" : "nein");
|
||||
if (mx.get(0) == awt.x && my.get(0) == awt.y) {
|
||||
target = mons.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMonitor == target) {
|
||||
// Bereits korrekt im Vollbild auf dem richtigen Monitor – kein Monitor-Wechsel,
|
||||
// da glfwSetWindowMonitor sonst den OverrideRedirect-Zustand stören kann.
|
||||
log.info("[Grafik] Fenster ist bereits Vollbild auf dem Ziel-Monitor – nur Fokus setzen");
|
||||
GLFW.glfwFocusWindow(win);
|
||||
return;
|
||||
}
|
||||
|
||||
// Falscher Monitor oder fensterbasierter Modus → Vollbild erzwingen
|
||||
GLFWVidMode vm = GLFW.glfwGetVideoMode(target);
|
||||
if (vm == null) {
|
||||
log.warn("[Grafik] Kein VideoMode für Ziel-Monitor");
|
||||
return;
|
||||
}
|
||||
log.info("[Grafik] Setze Vollbild: {}×{}@{}Hz (war: Monitor-Handle={})",
|
||||
vm.width(), vm.height(), vm.refreshRate(), currentMonitor);
|
||||
GLFW.glfwSetWindowMonitor(win, target, 0, 0, vm.width(), vm.height(), vm.refreshRate());
|
||||
GLFW.glfwFocusWindow(win);
|
||||
|
||||
} catch (Throwable e) {
|
||||
log.warn("[Grafik] Vollbild-Fix fehlgeschlagen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static AnimClip extractSubClip(AnimClip source, String name, float startSec, float endSec) {
|
||||
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||
for (AnimTrack<?> track : source.getTracks()) {
|
||||
@@ -350,6 +351,7 @@ public class AnimationLibrary extends BaseAppState {
|
||||
* bleibt frei für Sitz/Stand-Clips (leichte Neigungsbewegung)
|
||||
* Local Z → Höhe → IMMER frei lassen (Charakter-Höhe und Setz-Bewegung erhalten)
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
|
||||
if (clip == null || armature == null) return clip;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ public final class RetargetingSystem {
|
||||
return retarget(sourceClip, sourceArmature, targetArmature, MS_CORRECTIONS);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static AnimClip retarget(AnimClip sourceClip,
|
||||
Armature sourceArmature,
|
||||
Armature targetArmature,
|
||||
@@ -413,6 +414,7 @@ public final class RetargetingSystem {
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static AnimClip redirectTracks(AnimClip sourceClip, Armature targetArmature) {
|
||||
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||
for (AnimTrack<?> t : sourceClip.getTracks()) {
|
||||
@@ -462,6 +464,7 @@ public final class RetargetingSystem {
|
||||
*
|
||||
* Erstellt neue TransformTrack-Objekte, damit BinaryExporter die Änderung serialisiert.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static AnimClip stripRootXZTranslations(AnimClip clip, Armature armature) {
|
||||
if (clip == null || armature == null) return clip;
|
||||
log.info("[Strip] Clip '{}' – {} Tracks", clip.getName(), clip.getTracks().length);
|
||||
|
||||
@@ -1,59 +1,26 @@
|
||||
package de.blight.game.config;
|
||||
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.KeyInput;
|
||||
import com.jme3.input.RawInputListener;
|
||||
import com.jme3.input.event.*;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.lang.TextResolver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.font.BitmapFont;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.KeyInput;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.RawInputListener;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.input.event.JoyAxisEvent;
|
||||
import com.jme3.input.event.JoyButtonEvent;
|
||||
import com.jme3.input.event.KeyInputEvent;
|
||||
import com.jme3.input.event.MouseButtonEvent;
|
||||
import com.jme3.input.event.MouseMotionEvent;
|
||||
import com.jme3.input.event.TouchEvent;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector2f;
|
||||
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.lang.TextResolver;
|
||||
public class ConfigScreen extends MenuScreen implements RawInputListener {
|
||||
|
||||
public class ConfigScreen extends BaseAppState implements RawInputListener {
|
||||
|
||||
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
|
||||
private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
|
||||
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
|
||||
private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node guiNode;
|
||||
private BitmapFont font;
|
||||
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
|
||||
private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
|
||||
|
||||
private KeyBindings liveBindings;
|
||||
private KeyBindings editCopy;
|
||||
|
||||
private Runnable onSave;
|
||||
private Runnable onClose;
|
||||
|
||||
private Node panel;
|
||||
private Node bgLayer;
|
||||
private Node canvasNode;
|
||||
private List<Row> rows = new ArrayList<>();
|
||||
private int waitingRow = -1;
|
||||
|
||||
private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
|
||||
private float cancelBtnX, cancelBtnY;
|
||||
private Runnable onSave;
|
||||
private Runnable onClose;
|
||||
|
||||
private static class Row {
|
||||
String field;
|
||||
@@ -62,92 +29,68 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
|
||||
float x, y, w, h;
|
||||
}
|
||||
|
||||
private final List<Row> rows = new ArrayList<>();
|
||||
private int waitingRow = -1;
|
||||
|
||||
public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
|
||||
this.liveBindings = liveBindings;
|
||||
this.onSave = onSave;
|
||||
}
|
||||
|
||||
public boolean isWaiting() { return waitingRow >= 0; }
|
||||
public void setOnClose(Runnable onClose) { this.onClose = onClose; }
|
||||
public boolean isWaiting() { return waitingRow >= 0; }
|
||||
public void setOnClose(Runnable r) { this.onClose = r; }
|
||||
|
||||
public void cancelWaiting() {
|
||||
if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.guiNode = this.app.getGuiNode();
|
||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
}
|
||||
// ── MenuScreen ────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
protected void onEnableExtras() {
|
||||
editCopy = liveBindings.copy();
|
||||
waitingRow = -1;
|
||||
buildUI();
|
||||
app.getInputManager().setCursorVisible(true);
|
||||
app.getInputManager().addRawInputListener(this);
|
||||
app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
app.getInputManager().addListener(clickListener, "_CfgClick");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||
panel = null;
|
||||
protected void onDisableExtras() {
|
||||
rows.clear();
|
||||
waitingRow = -1;
|
||||
app.getInputManager().removeRawInputListener(this);
|
||||
app.getInputManager().deleteMapping("_CfgClick");
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) {}
|
||||
@Override
|
||||
protected void buildUI() {
|
||||
initPanel(t("menu.controls.title"));
|
||||
|
||||
private void buildUI() {
|
||||
float sw = MenuCanvas.REF_W;
|
||||
float sh = MenuCanvas.REF_H;
|
||||
|
||||
bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera());
|
||||
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||
guiNode.attachChild(bgLayer);
|
||||
guiNode.attachChild(canvasNode);
|
||||
|
||||
panel = new Node("cfg-panel");
|
||||
|
||||
float pw = 720, ph = 440;
|
||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
|
||||
|
||||
BitmapText title = text(t("menu.controls.title"), 20, COL_TEXT);
|
||||
centerText(title, px, py + ph - 40, pw);
|
||||
panel.attachChild(title);
|
||||
|
||||
BitmapText hint = text(t("menu.controls.hint"), 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
|
||||
centerText(hint, px, py + ph - 70, pw);
|
||||
BitmapText hint = crispText(t("menu.controls.hint"), UiTheme.FONT_SMALL,
|
||||
new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
|
||||
hint.setLocalTranslation(
|
||||
CONT_X + (CONT_W - hint.getLineWidth() / getUiScale()) / 2f,
|
||||
CONT_Y + CONT_H - HDR_H - 14f, -17f);
|
||||
panel.attachChild(hint);
|
||||
|
||||
float rowX = px + 30;
|
||||
float keyX = px + pw - 220;
|
||||
float rowW = 180;
|
||||
float rowH = 36;
|
||||
float startY = py + ph - 110;
|
||||
float stepY = 48;
|
||||
float rowLblX = CONT_X + 30f;
|
||||
float keyX = CONT_X + CONT_W - 220f;
|
||||
float rowW = 180f;
|
||||
float rowH = 36f;
|
||||
float startY = CONT_Y + CONT_H - HDR_H - 50f;
|
||||
float stepY = 48f;
|
||||
|
||||
for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
|
||||
String[] entry = KeyBindings.ENTRIES[i];
|
||||
float ry = startY - i * stepY;
|
||||
|
||||
BitmapText lbl = text(t("key." + entry[0]), 16, COL_TEXT);
|
||||
lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
|
||||
BitmapText lbl = crispText(t("key." + entry[0]), UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
|
||||
lbl.setLocalTranslation(rowLblX, ry + rowH - 4f, 0f);
|
||||
panel.attachChild(lbl);
|
||||
|
||||
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
|
||||
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0f);
|
||||
|
||||
BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
|
||||
kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
|
||||
BitmapText kt = crispText(KeyNames.of(editCopy.get(entry[0])),
|
||||
UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
|
||||
kt.setLocalTranslation(keyX + 10f, ry + rowH - 4f, 1f);
|
||||
panel.attachChild(kt);
|
||||
|
||||
Row row = new Row();
|
||||
@@ -158,61 +101,34 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
float btnW = 160, btnH = 42;
|
||||
float btnY = py + 25;
|
||||
saveBtnX = px + pw / 2f - btnW - 15;
|
||||
saveBtnY = btnY;
|
||||
saveBtnW = btnW;
|
||||
saveBtnH = btnH;
|
||||
cancelBtnX = px + pw / 2f + 15;
|
||||
cancelBtnY = btnY;
|
||||
float btnW = 160f, btnH = 42f;
|
||||
float btnY = CONT_Y + 22f;
|
||||
float saveX = CONT_X + CONT_W / 2f - btnW - 10f;
|
||||
float cancelX = CONT_X + CONT_W / 2f + 10f;
|
||||
|
||||
panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(saveBtnX, saveBtnY, btnW, btnH, 0));
|
||||
BitmapText saveLabel = text(t("menu.controls.btn.save"), 16, COL_TEXT);
|
||||
centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
|
||||
panel.attachChild(saveLabel);
|
||||
|
||||
panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelBtnX, cancelBtnY, btnW, btnH, 0));
|
||||
BitmapText cancelLabel = text(t("menu.controls.btn.cancel"), 16, COL_TEXT);
|
||||
centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
|
||||
panel.attachChild(cancelLabel);
|
||||
|
||||
canvasNode.attachChild(panel);
|
||||
addButton(panel, t("menu.controls.btn.save"), saveX, btnY, btnW, btnH, BtnStyle.SAVE, this::doSave);
|
||||
addButton(panel, t("menu.controls.btn.cancel"), cancelX, btnY, btnW, btnH, BtnStyle.QUIT, this::doClose);
|
||||
}
|
||||
|
||||
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
||||
if (!isPressed) return;
|
||||
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||
|
||||
@Override
|
||||
protected void onScreenClick(float[] v) {
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
Row r = rows.get(i);
|
||||
if (hits(v, r.x, r.y, r.w, r.h)) {
|
||||
if (v[0] >= r.x && v[0] <= r.x + r.w && v[1] >= r.y && v[1] <= r.y + r.h) {
|
||||
waitingRow = i;
|
||||
r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
|
||||
r.keyText.setText("...");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hits(v, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
|
||||
liveBindings.copyFrom(editCopy);
|
||||
KeyBindingStore.save(liveBindings);
|
||||
if (onSave != null) onSave.run();
|
||||
setEnabled(false);
|
||||
if (onClose != null) onClose.run();
|
||||
return;
|
||||
}
|
||||
|
||||
if (hits(v, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
|
||||
setEnabled(false);
|
||||
if (onClose != null) onClose.run();
|
||||
}
|
||||
};
|
||||
// ── RawInputListener ──────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyInputEvent evt) {
|
||||
if (!evt.isPressed() || waitingRow < 0) return;
|
||||
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return;
|
||||
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return;
|
||||
|
||||
Row r = rows.get(waitingRow);
|
||||
editCopy.set(r.field, evt.getKeyCode());
|
||||
@@ -221,20 +137,6 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
|
||||
waitingRow = -1;
|
||||
}
|
||||
|
||||
private void resetRowColor(int idx) {
|
||||
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
|
||||
}
|
||||
|
||||
private float[] toVirtual(Vector2f screen) {
|
||||
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||
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 };
|
||||
}
|
||||
|
||||
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||
|
||||
@Override public void beginInput() {}
|
||||
@Override public void endInput() {}
|
||||
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
|
||||
@@ -243,33 +145,23 @@ public class ConfigScreen extends BaseAppState implements RawInputListener {
|
||||
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
|
||||
@Override public void onTouchEvent(TouchEvent evt) {}
|
||||
|
||||
private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
|
||||
Geometry geo = new Geometry("q", new Quad(w, h));
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color.clone());
|
||||
if (color.a < 1f) {
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
}
|
||||
geo.setMaterial(mat);
|
||||
geo.setLocalTranslation(x, y, z);
|
||||
parent.attachChild(geo);
|
||||
return geo;
|
||||
// ── Private ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void doSave() {
|
||||
liveBindings.copyFrom(editCopy);
|
||||
KeyBindingStore.save(liveBindings);
|
||||
if (onSave != null) onSave.run();
|
||||
doClose();
|
||||
}
|
||||
|
||||
private BitmapText text(String content, int size, ColorRGBA color) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size);
|
||||
t.setColor(color);
|
||||
t.setText(content);
|
||||
return t;
|
||||
private void doClose() {
|
||||
setEnabled(false);
|
||||
if (onClose != null) onClose.run();
|
||||
}
|
||||
|
||||
private void centerText(BitmapText t, float x, float y, float width) {
|
||||
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
|
||||
private void resetRowColor(int idx) {
|
||||
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
|
||||
}
|
||||
|
||||
private boolean hits(float[] v, float x, float y, float w, float h) {
|
||||
return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h;
|
||||
}
|
||||
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||
}
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
package de.blight.game.config;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
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.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.system.AppSettings;
|
||||
import de.blight.lang.TextResolver;
|
||||
|
||||
public class GraphicsScreen extends BaseAppState {
|
||||
|
||||
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
|
||||
private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
|
||||
public class GraphicsScreen extends MenuScreen {
|
||||
|
||||
private static final int[][] RESOLUTIONS = {
|
||||
{1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
|
||||
@@ -33,32 +17,21 @@ public class GraphicsScreen extends BaseAppState {
|
||||
private static final int ROW_FULL = 1;
|
||||
private static final int ROW_VSYNC = 2;
|
||||
private static final int ROW_AA = 3;
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node guiNode;
|
||||
private BitmapFont font;
|
||||
private Node panel;
|
||||
private Node bgLayer;
|
||||
private Node canvasNode;
|
||||
private static final int ROW_COUNT = 4;
|
||||
|
||||
private final GraphicsSettings live;
|
||||
private GraphicsSettings edit;
|
||||
private final Runnable onClose;
|
||||
private GraphicsSettings edit;
|
||||
private final Runnable onClose;
|
||||
|
||||
private int resIdx;
|
||||
private int samplesIdx;
|
||||
|
||||
private final float[] cellX = new float[4];
|
||||
private final float[] cellY = new float[4];
|
||||
private final float[] cellW = new float[4];
|
||||
private final float cellH = 36;
|
||||
private final float arrW = 30;
|
||||
private final float[] leftX = new float[4];
|
||||
private final float[] rightX = new float[4];
|
||||
private final BitmapText[] valTexts = new BitmapText[4];
|
||||
|
||||
private float okX, okY, okW, okH;
|
||||
private float cancelX, cancelY;
|
||||
private final float arrW = 30f;
|
||||
private final float cellH = 36f;
|
||||
private final float[] cellX = new float[ROW_COUNT];
|
||||
private final float[] cellY = new float[ROW_COUNT];
|
||||
private final float[] cellW = new float[ROW_COUNT];
|
||||
private final BitmapText[] valTexts = new BitmapText[ROW_COUNT];
|
||||
|
||||
public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
|
||||
this.live = live;
|
||||
@@ -66,19 +39,12 @@ public class GraphicsScreen extends BaseAppState {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.guiNode = this.app.getGuiNode();
|
||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
protected void onEnableExtras() {
|
||||
edit = new GraphicsSettings();
|
||||
edit.width = live.width; edit.height = live.height;
|
||||
edit.fullscreen = live.fullscreen;
|
||||
edit.vsync = live.vsync;
|
||||
edit.samples = live.samples;
|
||||
edit.vsync = live.vsync;
|
||||
edit.samples = live.samples;
|
||||
|
||||
resIdx = 0;
|
||||
for (int i = 0; i < RESOLUTIONS.length; i++) {
|
||||
@@ -90,103 +56,56 @@ public class GraphicsScreen extends BaseAppState {
|
||||
for (int i = 0; i < SAMPLES.length; i++) {
|
||||
if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; }
|
||||
}
|
||||
|
||||
buildUI();
|
||||
app.getInputManager().setCursorVisible(true);
|
||||
app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
app.getInputManager().addListener(clickListener, "_GfxClick");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||
panel = null;
|
||||
app.getInputManager().deleteMapping("_GfxClick");
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
}
|
||||
protected void buildUI() {
|
||||
initPanel(t("menu.graphics.title"));
|
||||
|
||||
@Override protected void cleanup(Application app) {}
|
||||
|
||||
private void buildUI() {
|
||||
float sw = MenuCanvas.REF_W;
|
||||
float sh = MenuCanvas.REF_H;
|
||||
|
||||
bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera());
|
||||
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||
guiNode.attachChild(bgLayer);
|
||||
guiNode.attachChild(canvasNode);
|
||||
|
||||
panel = new Node("gfx-panel");
|
||||
|
||||
float pw = 640, ph = 400;
|
||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
|
||||
|
||||
BitmapText title = txt(t("menu.graphics.title"), 20, COL_TEXT);
|
||||
centerText(title, px, py + ph - 42, pw);
|
||||
panel.attachChild(title);
|
||||
|
||||
String[] labelKeys = {
|
||||
String[] lblKeys = {
|
||||
"menu.graphics.row.resolution",
|
||||
"menu.graphics.row.fullscreen",
|
||||
"menu.graphics.row.vsync",
|
||||
"menu.graphics.row.aa"
|
||||
};
|
||||
float lblX = px + 30;
|
||||
float vx = px + pw - 270;
|
||||
float vw = 190;
|
||||
float startY = py + ph - 100;
|
||||
float step = 60;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float lblX = CONT_X + 30f;
|
||||
float vx = CONT_X + CONT_W - 270f;
|
||||
float vw = 190f;
|
||||
float startY = CONT_Y + CONT_H - HDR_H - 58f;
|
||||
float step = 60f;
|
||||
|
||||
for (int i = 0; i < ROW_COUNT; i++) {
|
||||
final int row = i;
|
||||
float ry = startY - i * step;
|
||||
|
||||
BitmapText lbl = txt(t(labelKeys[i]), 16, COL_TEXT);
|
||||
lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
|
||||
BitmapText lbl = crispText(t(lblKeys[i]), UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
|
||||
lbl.setLocalTranslation(lblX, ry + cellH - 4f, 0f);
|
||||
panel.attachChild(lbl);
|
||||
|
||||
panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx - arrW - 6, ry, arrW, cellH, 0));
|
||||
BitmapText lt = txt("<", 16, COL_TEXT);
|
||||
lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
|
||||
panel.attachChild(lt);
|
||||
addButton(panel, "<", vx - arrW - 6f, ry, arrW, cellH,
|
||||
BtnStyle.ARROW, () -> cycleRow(row, -1));
|
||||
|
||||
panel.attachChild(NinePatch.button(app.getAssetManager()).build(vx, ry, vw, cellH, 0));
|
||||
panel.attachChild(NinePatch.button(assets).build(vx, ry, vw, cellH, 0f));
|
||||
|
||||
panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx + vw + 6, ry, arrW, cellH, 0));
|
||||
BitmapText rt = txt(">", 16, COL_TEXT);
|
||||
rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
|
||||
panel.attachChild(rt);
|
||||
addButton(panel, ">", vx + vw + 6f, ry, arrW, cellH,
|
||||
BtnStyle.ARROW, () -> cycleRow(row, +1));
|
||||
|
||||
BitmapText vt = txt("", 16, COL_TEXT_VAL);
|
||||
BitmapText vt = crispText("", UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
|
||||
panel.attachChild(vt);
|
||||
valTexts[i] = vt;
|
||||
|
||||
cellX[i] = vx; cellY[i] = ry; cellW[i] = vw;
|
||||
leftX[i] = vx - arrW - 6;
|
||||
rightX[i] = vx + vw + 6;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) refreshText(i);
|
||||
for (int i = 0; i < ROW_COUNT; i++) refreshText(i);
|
||||
|
||||
float bw = 160, bh = 42;
|
||||
okW = bw; okH = bh;
|
||||
okX = px + pw / 2f - bw - 10;
|
||||
okY = py + 22;
|
||||
cancelX = px + pw / 2f + 10;
|
||||
cancelY = py + 22;
|
||||
|
||||
panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(okX, okY, bw, bh, 0));
|
||||
BitmapText okLbl = txt(t("menu.graphics.btn.apply"), 16, COL_TEXT);
|
||||
centerText(okLbl, okX, okY + bh - 10, bw);
|
||||
panel.attachChild(okLbl);
|
||||
|
||||
panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelX, cancelY, bw, bh, 0));
|
||||
BitmapText cancelLbl = txt(t("menu.graphics.btn.cancel"), 16, COL_TEXT);
|
||||
centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
|
||||
panel.attachChild(cancelLbl);
|
||||
|
||||
canvasNode.attachChild(panel);
|
||||
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.graphics.btn.apply"), saveX, btnY, bw, bh, BtnStyle.SAVE, this::applyAndSave);
|
||||
addButton(panel, t("menu.graphics.btn.cancel"), canX, btnY, bw, bh, BtnStyle.QUIT, this::close);
|
||||
}
|
||||
|
||||
private void refreshText(int row) {
|
||||
@@ -195,31 +114,19 @@ public class GraphicsScreen extends BaseAppState {
|
||||
case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
|
||||
case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
|
||||
case ROW_AA -> SAMPLES[samplesIdx] == 0
|
||||
? t("menu.graphics.val.off")
|
||||
: SAMPLES[samplesIdx] + "x MSAA";
|
||||
? t("menu.graphics.val.off") : SAMPLES[samplesIdx] + "x MSAA";
|
||||
default -> "";
|
||||
};
|
||||
BitmapText vt = valTexts[row];
|
||||
vt.setText(val);
|
||||
float tw = vt.getLineWidth() / getUiScale();
|
||||
vt.setLocalTranslation(
|
||||
cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
|
||||
cellY[row] + cellH - 8,
|
||||
1
|
||||
cellX[row] + (cellW[row] - tw) / 2f,
|
||||
cellY[row] + cellH - 4f,
|
||||
1f
|
||||
);
|
||||
}
|
||||
|
||||
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
||||
if (!isPressed) return;
|
||||
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (hits(v, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; }
|
||||
if (hits(v, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; }
|
||||
}
|
||||
if (hits(v, okX, okY, okW, okH)) { applyAndSave(); return; }
|
||||
if (hits(v, cancelX, cancelY, okW, okH)) { close(); }
|
||||
};
|
||||
|
||||
private void cycleRow(int row, int dir) {
|
||||
switch (row) {
|
||||
case ROW_RES:
|
||||
@@ -227,12 +134,8 @@ public class GraphicsScreen extends BaseAppState {
|
||||
edit.width = RESOLUTIONS[resIdx][0];
|
||||
edit.height = RESOLUTIONS[resIdx][1];
|
||||
break;
|
||||
case ROW_FULL:
|
||||
edit.fullscreen = !edit.fullscreen;
|
||||
break;
|
||||
case ROW_VSYNC:
|
||||
edit.vsync = !edit.vsync;
|
||||
break;
|
||||
case ROW_FULL: edit.fullscreen = !edit.fullscreen; break;
|
||||
case ROW_VSYNC: edit.vsync = !edit.vsync; break;
|
||||
case ROW_AA:
|
||||
samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
|
||||
edit.samples = SAMPLES[samplesIdx];
|
||||
@@ -244,9 +147,8 @@ public class GraphicsScreen extends BaseAppState {
|
||||
private void applyAndSave() {
|
||||
live.width = edit.width; live.height = edit.height;
|
||||
live.fullscreen = edit.fullscreen;
|
||||
live.vsync = edit.vsync;
|
||||
live.samples = edit.samples;
|
||||
|
||||
live.vsync = edit.vsync;
|
||||
live.samples = edit.samples;
|
||||
GraphicsStore.save(live);
|
||||
|
||||
AppSettings s = app.getContext().getSettings();
|
||||
@@ -266,27 +168,5 @@ public class GraphicsScreen extends BaseAppState {
|
||||
if (onClose != null) onClose.run();
|
||||
}
|
||||
|
||||
private float[] toVirtual(Vector2f screen) {
|
||||
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||
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 };
|
||||
}
|
||||
|
||||
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA color) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size); t.setColor(color); t.setText(s);
|
||||
return t;
|
||||
}
|
||||
|
||||
private void centerText(BitmapText t, float x, float y, float width) {
|
||||
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
|
||||
}
|
||||
|
||||
private boolean hits(float[] v, float x, float y, float w, float h) {
|
||||
return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,12 @@ public class KeyBindings {
|
||||
public int sprint = KeyInput.KEY_LSHIFT;
|
||||
public int walk = KeyInput.KEY_LMENU;
|
||||
public int interact = KeyInput.KEY_E;
|
||||
public int turnLeft = KeyInput.KEY_Q;
|
||||
public int turnRight = KeyInput.KEY_E;
|
||||
public int inventory = KeyInput.KEY_I;
|
||||
public int quicksave = KeyInput.KEY_F5;
|
||||
public int character = KeyInput.KEY_C;
|
||||
public int quests = KeyInput.KEY_V;
|
||||
|
||||
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
||||
public static final String[][] ENTRIES = {
|
||||
@@ -26,8 +30,12 @@ public class KeyBindings {
|
||||
{"sprint", "Rennen"},
|
||||
{"walk", "Gehen"},
|
||||
{"interact", "Interagieren"},
|
||||
{"turnLeft", "Kamera links"},
|
||||
{"turnRight", "Kamera rechts"},
|
||||
{"inventory", "Inventar"},
|
||||
{"quicksave", "Schnellspeichern"},
|
||||
{"character", "Charakter"},
|
||||
{"quests", "Questlog"},
|
||||
};
|
||||
|
||||
public int get(String fieldName) {
|
||||
|
||||
@@ -1,51 +1,16 @@
|
||||
package de.blight.game.config;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
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.asset.AssetManager;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import de.blight.game.post.GaussianBlurFilter;
|
||||
import de.blight.game.scene.WorldScene;
|
||||
import de.blight.lang.TextResolver;
|
||||
|
||||
public class PauseMenu extends BaseAppState {
|
||||
public class PauseMenu extends MenuScreen {
|
||||
|
||||
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
|
||||
|
||||
private static final int BTN_GRAFIK = 0;
|
||||
private static final int BTN_AUDIO = 1;
|
||||
private static final int BTN_STEUERUNG = 2;
|
||||
private static final int BTN_SPEICHERN = 3;
|
||||
private static final int BTN_BEENDEN = 4;
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node guiNode;
|
||||
private BitmapFont font;
|
||||
private Node panel;
|
||||
private Node bgLayer;
|
||||
private Node canvasNode;
|
||||
|
||||
private Runnable onSave;
|
||||
private Runnable onGraphics;
|
||||
private Runnable onAudio;
|
||||
private Runnable onControls;
|
||||
|
||||
private GaussianBlurFilter blurFilter;
|
||||
|
||||
private final float[][] btnBounds = new float[5][4];
|
||||
private final Runnable onSave;
|
||||
private final Runnable onGraphics;
|
||||
private final Runnable onAudio;
|
||||
private final Runnable onControls;
|
||||
|
||||
public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onAudio, Runnable onControls) {
|
||||
this.onSave = onSave;
|
||||
@@ -55,139 +20,39 @@ public class PauseMenu extends BaseAppState {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.guiNode = this.app.getGuiNode();
|
||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
protected boolean withBlur() { return true; }
|
||||
|
||||
@Override
|
||||
protected Node buildBgLayer() {
|
||||
return MenuCanvas.createPauseBgLayer(assets, app.getCamera());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
buildUI();
|
||||
app.getInputManager().setCursorVisible(true);
|
||||
app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
app.getInputManager().addListener(clickListener, "_PauseClick");
|
||||
addBlur();
|
||||
}
|
||||
protected void buildUI() {
|
||||
initPanel(t("menu.pause.title"));
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
removeBlur();
|
||||
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||
panel = null;
|
||||
app.getInputManager().deleteMapping("_PauseClick");
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
}
|
||||
|
||||
private void addBlur() {
|
||||
WorldScene ws = getApplication().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 = getApplication().getStateManager().getState(WorldScene.class);
|
||||
if (ws != null) {
|
||||
FilterPostProcessor fpp = ws.getSharedFPP();
|
||||
if (fpp != null) fpp.removeFilter(blurFilter);
|
||||
}
|
||||
blurFilter = null;
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) {}
|
||||
|
||||
private void buildUI() {
|
||||
float sw = MenuCanvas.REF_W;
|
||||
float sh = MenuCanvas.REF_H;
|
||||
|
||||
bgLayer = MenuCanvas.createPauseBgLayer(app.getAssetManager(), app.getCamera());
|
||||
canvasNode = MenuCanvas.createFixedCanvas(app.getCamera());
|
||||
guiNode.attachChild(bgLayer);
|
||||
guiNode.attachChild(canvasNode);
|
||||
|
||||
panel = new Node("pause-panel");
|
||||
|
||||
float pw = 320, ph = 430;
|
||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
|
||||
|
||||
BitmapText title = txt(t("menu.pause.title"), 26, COL_TEXT);
|
||||
centerText(title, px, py + ph - 48, pw);
|
||||
panel.attachChild(title);
|
||||
|
||||
String[] labelKeys = {
|
||||
String[] keys = {
|
||||
"menu.pause.btn.graphics",
|
||||
"menu.pause.btn.audio",
|
||||
"menu.pause.btn.controls",
|
||||
"menu.pause.btn.save",
|
||||
"menu.pause.btn.quit"
|
||||
};
|
||||
Runnable[] acts = { onGraphics, onAudio, onControls, onSave, () -> app.stop() };
|
||||
BtnStyle[] styles = {
|
||||
BtnStyle.DEFAULT, BtnStyle.DEFAULT, BtnStyle.DEFAULT,
|
||||
BtnStyle.SAVE, BtnStyle.QUIT
|
||||
};
|
||||
|
||||
float bw = 260, bh = 52;
|
||||
float bx = px + (pw - bw) / 2f;
|
||||
float startY = py + ph - 112;
|
||||
float step = 62;
|
||||
float bw = 260f, bh = 52f;
|
||||
float bx = CONT_X + (CONT_W - bw) / 2f;
|
||||
float startY = CONT_Y + CONT_H - HDR_H - 72f;
|
||||
float step = 62f;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float by = startY - i * step;
|
||||
NinePatch btnPatch = switch (i) {
|
||||
case 3 -> NinePatch.buttonSave(app.getAssetManager());
|
||||
case 4 -> NinePatch.buttonQuit(app.getAssetManager());
|
||||
default -> NinePatch.button(app.getAssetManager());
|
||||
};
|
||||
panel.attachChild(btnPatch.build(bx, by, bw, bh, 0));
|
||||
|
||||
BitmapText lbl = txt(t(labelKeys[i]), 18, COL_TEXT);
|
||||
centerText(lbl, bx, by + bh - 16, bw);
|
||||
panel.attachChild(lbl);
|
||||
|
||||
btnBounds[i][0] = bx; btnBounds[i][1] = by;
|
||||
btnBounds[i][2] = bw; btnBounds[i][3] = bh;
|
||||
addButton(panel, t(keys[i]), bx, startY - i * step, bw, bh, styles[i], acts[i]);
|
||||
}
|
||||
|
||||
canvasNode.attachChild(panel);
|
||||
}
|
||||
|
||||
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
||||
if (!isPressed) return;
|
||||
Vector2f c = app.getInputManager().getCursorPosition();
|
||||
// Kein Scaling – einfach den Zentrumversatz abziehen
|
||||
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W) / 2f;
|
||||
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H) / 2f;
|
||||
float vx = c.x - ox;
|
||||
float vy = c.y - oy;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (!hits(vx, vy, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
|
||||
switch (i) {
|
||||
case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
|
||||
case BTN_AUDIO -> { if (onAudio != null) onAudio.run(); }
|
||||
case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
|
||||
case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); }
|
||||
case BTN_BEENDEN -> app.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA color) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size); t.setColor(color); t.setText(s);
|
||||
return t;
|
||||
}
|
||||
|
||||
private void centerText(BitmapText t, float x, float y, float width) {
|
||||
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
|
||||
}
|
||||
|
||||
private boolean hits(float px, float py, float x, float y, float w, float h) {
|
||||
return px >= x && px <= x + w && py >= y && py <= y + h;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package de.blight.game.control;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.AnalogListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseAxisTrigger;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Spatial;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
|
||||
/**
|
||||
* Third-Person-Kamera:
|
||||
@@ -17,6 +19,7 @@ import com.jme3.scene.Spatial;
|
||||
public class ThirdPersonCamera {
|
||||
|
||||
private static final float MOUSE_SENSITIVITY = 1.8f;
|
||||
private static final float KB_TURN_SPEED = 2.5f;
|
||||
private static final float BASE_DISTANCE = 5f;
|
||||
private static final float MIN_DISTANCE = BASE_DISTANCE - 3f;
|
||||
private static final float MAX_DISTANCE = BASE_DISTANCE + 3f;
|
||||
@@ -26,6 +29,7 @@ public class ThirdPersonCamera {
|
||||
|
||||
private final Camera cam;
|
||||
private final InputManager inputManager;
|
||||
private final KeyBindings keyBindings;
|
||||
|
||||
private Spatial target;
|
||||
|
||||
@@ -34,9 +38,10 @@ public class ThirdPersonCamera {
|
||||
private float distance = BASE_DISTANCE;
|
||||
private boolean paused = false;
|
||||
|
||||
public ThirdPersonCamera(Camera cam, InputManager inputManager) {
|
||||
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
|
||||
this.cam = cam;
|
||||
this.inputManager = inputManager;
|
||||
this.keyBindings = keyBindings;
|
||||
registerMappings();
|
||||
}
|
||||
|
||||
@@ -49,19 +54,24 @@ public class ThirdPersonCamera {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private void registerMappings() {
|
||||
inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false));
|
||||
inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true));
|
||||
inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
|
||||
inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
|
||||
inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false));
|
||||
inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true));
|
||||
inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
|
||||
inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
|
||||
inputManager.addMapping("CamTurnLeft", new KeyTrigger(keyBindings.turnLeft));
|
||||
inputManager.addMapping("CamTurnRight", new KeyTrigger(keyBindings.turnRight));
|
||||
|
||||
AnalogListener analogListener = (name, value, tpf) -> {
|
||||
if (paused) return;
|
||||
switch (name) {
|
||||
// Horizontale Rotation
|
||||
case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
|
||||
case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
|
||||
// Horizontale Rotation — Maus
|
||||
case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
|
||||
case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
|
||||
// Horizontale Rotation — Tastatur
|
||||
case "CamTurnLeft" -> yaw += value * KB_TURN_SPEED;
|
||||
case "CamTurnRight" -> yaw -= value * KB_TURN_SPEED;
|
||||
// Vertikale Rotation — Y invertiert: Maus hoch → Kamera runter
|
||||
case "MouseY" -> pitch = FastMath.clamp(pitch - value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
|
||||
case "MouseYNeg" -> pitch = FastMath.clamp(pitch + value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
|
||||
@@ -71,7 +81,8 @@ public class ThirdPersonCamera {
|
||||
}
|
||||
};
|
||||
inputManager.addListener(analogListener,
|
||||
"MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut");
|
||||
"MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut",
|
||||
"CamTurnLeft", "CamTurnRight");
|
||||
}
|
||||
|
||||
public void update(float tpf) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.jme3.scene.shape.*;
|
||||
import com.jme3.shadow.*;
|
||||
import com.jme3.terrain.geomipmap.*;
|
||||
import com.jme3.texture.*;
|
||||
import com.jme3.texture.image.ColorSpace;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import java.nio.ByteBuffer;
|
||||
import de.blight.common.MapData;
|
||||
@@ -298,7 +299,7 @@ public class WorldScene extends BaseAppState {
|
||||
log.warn("[WorldScene] PathFinder nicht ladbar – Navigation deaktiviert: {}", e.getMessage());
|
||||
}
|
||||
|
||||
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
|
||||
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager(), keyBindings);
|
||||
thirdPersonCam.setTarget(character);
|
||||
|
||||
MainCharacter mc = findMainCharacter();
|
||||
@@ -322,6 +323,14 @@ public class WorldScene extends BaseAppState {
|
||||
inventoryState.setEnabled(false);
|
||||
app.getStateManager().attach(inventoryState);
|
||||
app.getStateManager().attach(new de.blight.game.state.HudState(mc));
|
||||
app.getStateManager().attach(new de.blight.game.state.HotbarState(mc));
|
||||
app.getStateManager().attach(new de.blight.game.state.CompassHudState());
|
||||
de.blight.game.state.CharacterState charState = new de.blight.game.state.CharacterState(mc);
|
||||
charState.setEnabled(false);
|
||||
app.getStateManager().attach(charState);
|
||||
de.blight.game.state.QuestState questState = new de.blight.game.state.QuestState(mc);
|
||||
questState.setEnabled(false);
|
||||
app.getStateManager().attach(questState);
|
||||
}
|
||||
|
||||
// Ertrinken-System (Wassertiefe > 1,8 m → Teleport zum Strand)
|
||||
@@ -907,7 +916,7 @@ public class WorldScene extends BaseAppState {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
try {
|
||||
ByteBuffer buf = loadTextureToRGBA8(path, size, am);
|
||||
images.add(new Image(Image.Format.RGBA8, size, size, buf));
|
||||
images.add(new Image(Image.Format.RGBA8, size, size, buf, ColorSpace.Linear));
|
||||
continue;
|
||||
} catch (Exception e) { log.warn("[WorldScene-Array] Slot {} nicht ladbar: {}", i, e.getMessage()); }
|
||||
}
|
||||
@@ -916,7 +925,7 @@ public class WorldScene extends BaseAppState {
|
||||
b = (byte)(fb.b * 255), a = (byte)(fb.a * 255);
|
||||
for (int p = 0; p < size * size; p++) buf.put(r).put(g).put(b).put(a);
|
||||
buf.flip();
|
||||
images.add(new Image(Image.Format.RGBA8, size, size, buf));
|
||||
images.add(new Image(Image.Format.RGBA8, size, size, buf, ColorSpace.Linear));
|
||||
}
|
||||
com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images);
|
||||
texArr.setWrap(Texture.WrapMode.Repeat);
|
||||
@@ -1053,7 +1062,7 @@ public class WorldScene extends BaseAppState {
|
||||
splatBuf.put(splatR[i]).put(map.splatG[i]).put(map.splatB[i]).put(map.splatA[i]);
|
||||
}
|
||||
splatBuf.flip();
|
||||
Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf));
|
||||
Texture2D splatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, splatBuf, ColorSpace.Linear));
|
||||
splatTex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
splatTex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
@@ -1066,7 +1075,7 @@ public class WorldScene extends BaseAppState {
|
||||
.put(map.upperSplatB[i]).put(map.upperSplatA[i]);
|
||||
}
|
||||
upperBuf.flip();
|
||||
Texture2D upperSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, upperBuf));
|
||||
Texture2D upperSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, upperBuf, ColorSpace.Linear));
|
||||
upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
upperSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
@@ -1083,7 +1092,7 @@ public class WorldScene extends BaseAppState {
|
||||
thirdBuf.put(map.thirdSplatA[i]);
|
||||
}
|
||||
thirdBuf.flip();
|
||||
Texture2D thirdSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, thirdBuf));
|
||||
Texture2D thirdSplatTex = new Texture2D(new Image(Image.Format.RGBA8, sz, sz, thirdBuf, ColorSpace.Linear));
|
||||
thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
|
||||
thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
|
||||
thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
|
||||
@@ -1122,7 +1131,7 @@ public class WorldScene extends BaseAppState {
|
||||
buf.put((byte)(color.r * 255)).put((byte)(color.g * 255))
|
||||
.put((byte)(color.b * 255)).put((byte)(color.a * 255));
|
||||
buf.flip();
|
||||
return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf));
|
||||
return new Texture2D(new Image(Image.Format.RGBA8, 1, 1, buf, ColorSpace.Linear));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.KeyInput;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.AnalogListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseAxisTrigger;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.XPHelper;
|
||||
import de.blight.common.model.abilities.Abilities;
|
||||
import de.blight.game.config.NinePatch;
|
||||
import de.blight.game.config.OverlayState;
|
||||
import de.blight.game.config.UiTheme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Charakter-Übersicht: Werte (links) und gelernte Fähigkeiten mit Scrollbar (rechts).
|
||||
*/
|
||||
public class CharacterState extends OverlayState {
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final float LEFT_W = 286f;
|
||||
private static final float RIGHT_X = PX + PAD + LEFT_W + 12f;
|
||||
private static final float RIGHT_W = PW - PAD - LEFT_W - PAD - 12f - PAD;
|
||||
|
||||
private static final float SB_W = 12f;
|
||||
private static final float SB_GAP = 4f;
|
||||
private static final int COLS = 3;
|
||||
private static final float CARD_GAP = 6f;
|
||||
private static final float CONTENT_W = RIGHT_W - SB_W - SB_GAP;
|
||||
private static final float CARD_W = (CONTENT_W - (COLS - 1) * CARD_GAP) / COLS;
|
||||
|
||||
private static final float COMBAT_CARD_H = 140f;
|
||||
private static final float CRAFT_CARD_H = 110f;
|
||||
private static final float SECT_H = 26f;
|
||||
private static final float BAR_W_STAT = 180f;
|
||||
private static final float BAR_H_STAT = 12f;
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final String MAP_TOGGLE = "_CharToggle";
|
||||
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
||||
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
||||
|
||||
// ── Scroll-Zustand ────────────────────────────────────────────────────────
|
||||
|
||||
private Node contentPanel;
|
||||
private Node skillsNode;
|
||||
private Node scrollbarNode;
|
||||
private float skillScrollY = 0f;
|
||||
private float skillContentH = 0f;
|
||||
private float skillViewH = 0f;
|
||||
private boolean needsScrollbar = false;
|
||||
|
||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private final MainCharacter mc;
|
||||
|
||||
private record Skill(String name, int lvl, int maxLvl, String[][] abils) {}
|
||||
|
||||
public CharacterState(MainCharacter mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
// ── OverlayState-Implementierung ──────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected String getTitle() { return "Charakter"; }
|
||||
|
||||
@Override
|
||||
protected void initOverlayKeys() {
|
||||
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(KeyInput.KEY_C));
|
||||
app.getInputManager().addListener(toggleListener, MAP_TOGGLE);
|
||||
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
app.getInputManager().addListener(scrollAnalog, MAP_SCROLL_UP, MAP_SCROLL_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupOverlayKeys() {
|
||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildContent(Node panel, float scale) {
|
||||
float divX = PX + PAD + LEFT_W + 6f;
|
||||
addQuad(panel, divX, PY + PAD, 1f, PH - HDR_H - PAD * 2, UiTheme.COL_DIVIDER, -17f);
|
||||
buildStats(panel);
|
||||
buildAbilities(panel);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) { super.cleanup(application); }
|
||||
|
||||
// ── Listener ──────────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener toggleListener = (name, pressed, tpf) -> {
|
||||
if (pressed) { setEnabled(!isEnabled()); }
|
||||
};
|
||||
|
||||
private final AnalogListener scrollAnalog = (name, value, tpf) -> {
|
||||
if (!isEnabled() || !needsScrollbar) { return; }
|
||||
float delta = value * 60f;
|
||||
if (MAP_SCROLL_UP.equals(name)) {
|
||||
skillScrollY = Math.max(0f, skillScrollY - delta);
|
||||
} else {
|
||||
float maxScroll = Math.max(0f, skillContentH - skillViewH);
|
||||
skillScrollY = Math.min(maxScroll, skillScrollY + delta);
|
||||
}
|
||||
rebuildSkills();
|
||||
};
|
||||
|
||||
// ── Linke Spalte: Werte ───────────────────────────────────────────────────
|
||||
|
||||
private void buildStats(Node panel) {
|
||||
float x = PX + PAD;
|
||||
float y = PY + PH - HDR_H - PAD;
|
||||
|
||||
String name = mc.getCharacterId() != null ? mc.getCharacterId() : "Held";
|
||||
String displayName = name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
|
||||
BitmapText nameTxt = crispText(displayName, UiTheme.FONT_TITLE, UiTheme.COL_GOLD);
|
||||
nameTxt.setLocalTranslation(x, y, -17);
|
||||
panel.attachChild(nameTxt);
|
||||
y -= 26;
|
||||
|
||||
int lvl = mc.getLevel();
|
||||
int curXp = mc.getXp();
|
||||
int reqXp = XPHelper.getXpRequired(lvl);
|
||||
BitmapText levelTxt = crispText("Level " + (lvl + 1), UiTheme.FONT_HEADING, UiTheme.COL_WHITE);
|
||||
levelTxt.setLocalTranslation(x, y, -17);
|
||||
panel.attachChild(levelTxt);
|
||||
y -= 16;
|
||||
|
||||
float xpRatio = reqXp > 0 ? Math.min(1f, (float) curXp / reqXp) : 0f;
|
||||
addQuad(panel, x, y - BAR_H_STAT, BAR_W_STAT, BAR_H_STAT, UiTheme.COL_BAR_BG, -17);
|
||||
if (xpRatio > 0f) {
|
||||
addQuad(panel, x, y - BAR_H_STAT, BAR_W_STAT * xpRatio, BAR_H_STAT, UiTheme.COL_XP_BAR, -16);
|
||||
}
|
||||
BitmapText xpTxt = crispText(curXp + " / " + reqXp + " XP", UiTheme.FONT_TINY, UiTheme.COL_MUTED);
|
||||
xpTxt.setLocalTranslation(x, y - BAR_H_STAT - 2, -16);
|
||||
panel.attachChild(xpTxt);
|
||||
y -= BAR_H_STAT + 18;
|
||||
|
||||
addQuad(panel, x, y, LEFT_W - 6, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||
y -= 14;
|
||||
|
||||
y = statBar(panel, "HP", mc.getCurrentHp(), mc.getMaxHp(), UiTheme.COL_HP, x, y);
|
||||
y = statBar(panel, "Stamina", mc.getCurrentStamina(), mc.getMaxStamina(), UiTheme.COL_STAMINA, x, y);
|
||||
y = statBar(panel, "Mana", mc.getCurrentMana(), mc.getMaxMana(), UiTheme.COL_MANA, x, y);
|
||||
|
||||
y -= 8;
|
||||
addQuad(panel, x, y, LEFT_W - 6, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||
y -= 16;
|
||||
|
||||
int hpReg = mc.getOpenHpRegeneration();
|
||||
int mnReg = mc.getOpenManaRegeneration();
|
||||
int stReg = mc.getOpenStaminaRegeneration();
|
||||
BitmapText regenLbl = crispText("Regeneration", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
regenLbl.setLocalTranslation(x, y, -17);
|
||||
panel.attachChild(regenLbl);
|
||||
y -= 18;
|
||||
regenLine(panel, "HP", hpReg, x, y); y -= 16;
|
||||
regenLine(panel, "Stamina", stReg, x, y); y -= 16;
|
||||
regenLine(panel, "Mana", mnReg, x, y);
|
||||
}
|
||||
|
||||
private float statBar(Node parent, String label, int cur, int max, ColorRGBA col,
|
||||
float x, float y) {
|
||||
float ratio = max > 0 ? Math.min(1f, (float) cur / max) : 0f;
|
||||
BitmapText lblTxt = crispText(label, UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
lblTxt.setLocalTranslation(x, y, -17);
|
||||
parent.attachChild(lblTxt);
|
||||
BitmapText valTxt = crispText(cur + " / " + max, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
|
||||
valTxt.setLocalTranslation(x + 70, y, -17);
|
||||
parent.attachChild(valTxt);
|
||||
y -= 16;
|
||||
addQuad(parent, x, y - BAR_H_STAT, BAR_W_STAT, BAR_H_STAT, UiTheme.COL_BAR_BG, -17);
|
||||
if (ratio > 0f) {
|
||||
addQuad(parent, x, y - BAR_H_STAT, BAR_W_STAT * ratio, BAR_H_STAT, col, -16);
|
||||
}
|
||||
return y - BAR_H_STAT - 10;
|
||||
}
|
||||
|
||||
private void regenLine(Node parent, String label, int val, float x, float y) {
|
||||
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
|
||||
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
lblTxt.setLocalTranslation(x, y, -17);
|
||||
parent.attachChild(lblTxt);
|
||||
BitmapText valTxt = crispText(val > 0 ? "+" + val : "—", UiTheme.FONT_BODY, col);
|
||||
valTxt.setLocalTranslation(x + 70, y, -17);
|
||||
parent.attachChild(valTxt);
|
||||
}
|
||||
|
||||
// ── Rechte Spalte: Fähigkeiten ────────────────────────────────────────────
|
||||
|
||||
private void buildAbilities(Node panel) {
|
||||
Abilities ab = mc.getAbilities();
|
||||
if (ab == null) { return; }
|
||||
|
||||
skillScrollY = 0f;
|
||||
contentPanel = panel;
|
||||
skillsNode = null;
|
||||
scrollbarNode = null;
|
||||
skillViewH = PH - HDR_H - PAD * 2;
|
||||
|
||||
List<Skill> combat = learnedCombat(ab);
|
||||
List<Skill> craft = learnedCraft(ab);
|
||||
skillContentH = calcContentH(combat, craft);
|
||||
needsScrollbar = skillContentH > skillViewH;
|
||||
|
||||
skillsNode = new Node("skills");
|
||||
buildSkillsContent(skillsNode, combat, craft);
|
||||
panel.attachChild(skillsNode);
|
||||
|
||||
if (needsScrollbar) {
|
||||
scrollbarNode = buildScrollbarNode();
|
||||
panel.attachChild(scrollbarNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildSkills() {
|
||||
if (contentPanel == null) { return; }
|
||||
if (skillsNode != null) { contentPanel.detachChild(skillsNode); skillsNode = null; }
|
||||
if (scrollbarNode != null) { contentPanel.detachChild(scrollbarNode); scrollbarNode = null; }
|
||||
|
||||
Abilities ab = mc.getAbilities();
|
||||
if (ab == null) { return; }
|
||||
List<Skill> combat = learnedCombat(ab);
|
||||
List<Skill> craft = learnedCraft(ab);
|
||||
|
||||
skillsNode = new Node("skills");
|
||||
buildSkillsContent(skillsNode, combat, craft);
|
||||
contentPanel.attachChild(skillsNode);
|
||||
|
||||
if (needsScrollbar) {
|
||||
scrollbarNode = buildScrollbarNode();
|
||||
contentPanel.attachChild(scrollbarNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildSkillsContent(Node out, List<Skill> combat, List<Skill> craft) {
|
||||
float visTop = PY + PH - HDR_H - PAD;
|
||||
float visBot = PY + PAD;
|
||||
|
||||
if (combat.isEmpty() && craft.isEmpty()) {
|
||||
float cy = PY + PAD + skillViewH * 0.5f;
|
||||
BitmapText line1 = crispText("— Noch keine Fähigkeiten erlernt —",
|
||||
UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
line1.setLocalTranslation(RIGHT_X + 8, cy + 8, -17f);
|
||||
out.attachChild(line1);
|
||||
BitmapText line2 = crispText("Fähigkeiten erscheinen hier, sobald du Punkte investierst.",
|
||||
UiTheme.FONT_SMALL, UiTheme.COL_LOCKED);
|
||||
line2.setLocalTranslation(RIGHT_X + 8, cy - 8, -17f);
|
||||
out.attachChild(line2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Virtueller Y-Cursor (ohne Scroll), zählt von oben nach unten
|
||||
float vy = visTop;
|
||||
|
||||
if (!combat.isEmpty()) {
|
||||
if (inView(vy - SECT_H, vy, visBot, visTop)) {
|
||||
addQuad(out, RIGHT_X, vy - SECT_H - skillScrollY, CONTENT_W, SECT_H,
|
||||
UiTheme.COL_SECTION, -18f);
|
||||
BitmapText t = crispText("Kampf", UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
|
||||
t.setLocalTranslation(RIGHT_X + 8, vy - 6 - skillScrollY, -17f);
|
||||
out.attachChild(t);
|
||||
}
|
||||
vy -= SECT_H + 4;
|
||||
|
||||
int combatRows = (combat.size() + COLS - 1) / COLS;
|
||||
for (int i = 0; i < combat.size(); i++) {
|
||||
int col = i % COLS;
|
||||
int row = i / COLS;
|
||||
float cx = RIGHT_X + col * (CARD_W + CARD_GAP);
|
||||
float top = vy - row * (COMBAT_CARD_H + CARD_GAP);
|
||||
float bot = top - COMBAT_CARD_H;
|
||||
if (inView(bot, top, visBot, visTop)) {
|
||||
Skill s = combat.get(i);
|
||||
buildSkillCard(out, s.name(), s.lvl(), s.maxLvl(), s.abils(),
|
||||
cx, bot - skillScrollY, CARD_W, COMBAT_CARD_H);
|
||||
}
|
||||
}
|
||||
vy -= combatRows * (COMBAT_CARD_H + CARD_GAP) + 12;
|
||||
}
|
||||
|
||||
if (!craft.isEmpty()) {
|
||||
if (inView(vy - SECT_H, vy, visBot, visTop)) {
|
||||
addQuad(out, RIGHT_X, vy - SECT_H - skillScrollY, CONTENT_W, SECT_H,
|
||||
UiTheme.COL_SECTION, -18f);
|
||||
BitmapText t = crispText("Handwerk", UiTheme.FONT_LABEL, UiTheme.COL_GOLD);
|
||||
t.setLocalTranslation(RIGHT_X + 8, vy - 6 - skillScrollY, -17f);
|
||||
out.attachChild(t);
|
||||
}
|
||||
vy -= SECT_H + 4;
|
||||
|
||||
for (int i = 0; i < craft.size(); i++) {
|
||||
int col = i % COLS;
|
||||
int row = i / COLS;
|
||||
float cx = RIGHT_X + col * (CARD_W + CARD_GAP);
|
||||
float top = vy - row * (CRAFT_CARD_H + CARD_GAP);
|
||||
float bot = top - CRAFT_CARD_H;
|
||||
if (inView(bot, top, visBot, visTop)) {
|
||||
Skill s = craft.get(i);
|
||||
buildSkillCard(out, s.name(), s.lvl(), s.maxLvl(), s.abils(),
|
||||
cx, bot - skillScrollY, CARD_W, CRAFT_CARD_H);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean inView(float virtualBot, float virtualTop, float visBot, float visTop) {
|
||||
float sBot = virtualBot - skillScrollY;
|
||||
float sTop = virtualTop - skillScrollY;
|
||||
return sTop > visBot && sBot < visTop;
|
||||
}
|
||||
|
||||
private float calcContentH(List<Skill> combat, List<Skill> craft) {
|
||||
float h = 0;
|
||||
if (!combat.isEmpty()) {
|
||||
int rows = (combat.size() + COLS - 1) / COLS;
|
||||
h += SECT_H + 4 + rows * (COMBAT_CARD_H + CARD_GAP) + 12;
|
||||
}
|
||||
if (!craft.isEmpty()) {
|
||||
int rows = (craft.size() + COLS - 1) / COLS;
|
||||
h += SECT_H + 4 + rows * (CRAFT_CARD_H + CARD_GAP);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
private Node buildScrollbarNode() {
|
||||
Node sb = new Node("scrollbar");
|
||||
float sbX = RIGHT_X + CONTENT_W + SB_GAP;
|
||||
float bot = PY + PAD;
|
||||
|
||||
sb.attachChild(NinePatch.scrollbarTrack(assets).build(sbX, bot, SB_W, skillViewH, -17f));
|
||||
|
||||
float maxScroll = skillContentH - skillViewH;
|
||||
float thumbH = Math.max(20f, skillViewH * (skillViewH / skillContentH));
|
||||
float ratio = maxScroll > 0 ? skillScrollY / maxScroll : 0f;
|
||||
float thumbY = bot + (skillViewH - thumbH) * (1f - ratio);
|
||||
sb.attachChild(NinePatch.scrollbarThumb(assets).build(sbX, thumbY, SB_W, thumbH, -16f));
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
// ── Skill-Karte ───────────────────────────────────────────────────────────
|
||||
|
||||
private void buildSkillCard(Node panel, String name, int lvl, int maxLvl,
|
||||
String[][] abils, float x, float y, float w, float h) {
|
||||
addQuad(panel, x, y, w, h, UiTheme.COL_CARD_BRD, -18);
|
||||
addQuad(panel, x+1, y+1, w-2, h-2, UiTheme.COL_CARD, -17);
|
||||
|
||||
float iy = y + h - 4;
|
||||
iy -= 16;
|
||||
String displayName = name.length() > 14 ? name.substring(0, 13) + "…" : name;
|
||||
BitmapText nameTxt = crispText(displayName, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
|
||||
nameTxt.setLocalTranslation(x + 5, iy, -16);
|
||||
panel.attachChild(nameTxt);
|
||||
iy -= 4;
|
||||
|
||||
iy -= 10;
|
||||
float pipSz = 7f;
|
||||
float pipGap = 2f;
|
||||
float totalPips = maxLvl * (pipSz + pipGap) - pipGap;
|
||||
float pipX = x + (w - totalPips) / 2f;
|
||||
for (int p = 0; p < maxLvl; p++) {
|
||||
ColorRGBA pipCol = p < lvl ? UiTheme.COL_PIP_ON : UiTheme.COL_PIP_OFF;
|
||||
addQuad(panel, pipX + p * (pipSz + pipGap), iy - pipSz, pipSz, pipSz, pipCol, -16);
|
||||
}
|
||||
BitmapText lvlTxt = crispText("Lv. " + lvl, UiTheme.FONT_TINY, UiTheme.COL_MUTED);
|
||||
lvlTxt.setLocalTranslation(x + 5, iy, -16);
|
||||
panel.attachChild(lvlTxt);
|
||||
iy -= pipSz + 6;
|
||||
|
||||
addQuad(panel, x + 4, iy, w - 8, 1f, UiTheme.COL_DIVIDER, -16f);
|
||||
iy -= 12;
|
||||
|
||||
for (String[] abil : abils) {
|
||||
if (iy < y + 4) { break; }
|
||||
String abilName = abil[0];
|
||||
int minLvl = Integer.parseInt(abil[1]);
|
||||
boolean unlocked = lvl >= minLvl;
|
||||
ColorRGBA col = unlocked ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
|
||||
String prefix = unlocked ? "✓ " : "○ ";
|
||||
BitmapText abilTxt = crispText(prefix + abilName, UiTheme.FONT_SMALL, col);
|
||||
abilTxt.setLocalTranslation(x + 5, iy, -16);
|
||||
panel.attachChild(abilTxt);
|
||||
iy -= 14;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fähigkeitenlisten (nur gelernte) ──────────────────────────────────────
|
||||
|
||||
private List<Skill> learnedCombat(Abilities ab) {
|
||||
List<Skill> list = new ArrayList<>();
|
||||
if (ab.getLvlSwordsmanship() > 0)
|
||||
list.add(new Skill("Schwertkampf", ab.getLvlSwordsmanship(), 10, swordAbilNames()));
|
||||
if (ab.getLvlStaffCombat() > 0)
|
||||
list.add(new Skill("Stabkampf", ab.getLvlStaffCombat(), 10, staffAbilNames()));
|
||||
if (ab.getLvlMagic() > 0)
|
||||
list.add(new Skill("Magie", ab.getLvlMagic(), 10, magicAbilNames()));
|
||||
if (ab.getLvlHeavyWeapons() > 0)
|
||||
list.add(new Skill("Schwere Waffen", ab.getLvlHeavyWeapons(), 10, heavyAbilNames()));
|
||||
if (ab.getLvlArchery() > 0)
|
||||
list.add(new Skill("Bogenschuss", ab.getLvlArchery(), 10, archerAbilNames()));
|
||||
if (ab.getLvlCrossbow() > 0)
|
||||
list.add(new Skill("Armbrust", ab.getLvlCrossbow(), 10, crossbowAbilNames()));
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<Skill> learnedCraft(Abilities ab) {
|
||||
List<Skill> list = new ArrayList<>();
|
||||
if (ab.getLvlThievery() > 0)
|
||||
list.add(new Skill("Diebstahl",
|
||||
ab.getLvlThievery(), 3, craftDescs("Schlösser knacken", "Taschendiebstahl", "Schleichen")));
|
||||
if (ab.getLvlAlchemy() > 0)
|
||||
list.add(new Skill("Alchemie",
|
||||
ab.getLvlAlchemy(), 3, craftDescs("Kräuter-Tränke", "Temp. Verstärkung", "Dauer-Tränke")));
|
||||
if (ab.getLvlEngineering() > 0)
|
||||
list.add(new Skill("Ingenieurskunst",
|
||||
ab.getLvlEngineering(), 3, craftDescs("Bandagen & Spritzen", "Wurfbomben", "Artillerie")));
|
||||
if (ab.getLvlSmithery() > 0)
|
||||
list.add(new Skill("Schmiedekunst",
|
||||
ab.getLvlSmithery(), 3, craftDescs("Einfache Waffen", "Mittlere Ausrüst.", "Meisterwerke")));
|
||||
if (ab.getLvlEnchanting() > 0)
|
||||
list.add(new Skill("Verzauberung",
|
||||
ab.getLvlEnchanting(), 3, craftDescs("Schwache Gegenstände", "Mittelst. Gegenstände", "Mächtige Gegenstände")));
|
||||
return list;
|
||||
}
|
||||
|
||||
// ── Fähigkeiten-Daten ─────────────────────────────────────────────────────
|
||||
|
||||
private static String[][] swordAbilNames() {
|
||||
return new String[][]{{"Basisangriff","1"},{"Block","3"},{"Schwerer Schlag","5"},
|
||||
{"Gezielter Stich","7"},{"Klingentanz","9"},{"Todesstoß","10"}};
|
||||
}
|
||||
private static String[][] staffAbilNames() {
|
||||
return new String[][]{{"Basisangriff","1"},{"Block","3"},{"Schwerer Schlag","5"},
|
||||
{"Standfestigkeit","7"},{"Finte","9"},{"Entwaffnen","10"}};
|
||||
}
|
||||
private static String[][] magicAbilNames() {
|
||||
return new String[][]{{"Feuerball","1"},{"Licht","1"},{"Schild","3"},
|
||||
{"Verwurzelung","5"},{"Schockwelle","7"},{"Lebensraub","9"},{"Kettenblitz","10"}};
|
||||
}
|
||||
private static String[][] heavyAbilNames() {
|
||||
return new String[][]{{"Basisangriff","1"},{"Block","3"},{"Schwerer Schlag","5"},
|
||||
{"Wirbelangriff","7"},{"Doppelangriff","9"},{"Exekution","10"}};
|
||||
}
|
||||
private static String[][] archerAbilNames() {
|
||||
return new String[][]{{"Schuss","1"},{"Präzisionsschuss","3"},{"Schnelles Laden","5"},
|
||||
{"Durchschlag","7"},{"Doppelschuss","9"},{"Adlerauge","10"}};
|
||||
}
|
||||
private static String[][] crossbowAbilNames() {
|
||||
return new String[][]{{"Schuss","1"},{"Zielsicherheit","3"},{"Schnelles Laden","5"},
|
||||
{"Durchschlag","7"},{"Doppelbolzen","9"},{"Scharfschütze","10"}};
|
||||
}
|
||||
private static String[][] craftDescs(String d1, String d2, String d3) {
|
||||
return new String[][]{{d1,"1"},{d2,"2"},{d3,"3"}};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.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.math.FastMath;
|
||||
import com.jme3.math.Vector3f;
|
||||
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.Spatial;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
|
||||
/**
|
||||
* Kompass unten links. Zeigt N/O/S/W-Labels, die um das Kompasszentrum
|
||||
* rotieren wenn sich die Kamera dreht. Eine feste Markierung oben
|
||||
* zeigt immer die aktuelle Blickrichtung an.
|
||||
*/
|
||||
public class CompassHudState extends BaseAppState {
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────
|
||||
private static final float SIZE = 76f; // Kompass-Durchmesser (Pixel)
|
||||
private static final float RADIUS = 26f; // Label-Radius vom Mittelpunkt
|
||||
private static final float MARGIN = 16f; // Abstand zum Bildschirmrand
|
||||
private static final float LABEL_Z = 4f;
|
||||
private static final float DOT_SZ = 6f; // Vorwärts-Marker
|
||||
|
||||
private static final ColorRGBA COL_BG = new ColorRGBA(0.06f, 0.06f, 0.10f, 0.88f);
|
||||
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.30f, 0.30f, 0.44f, 1.00f);
|
||||
private static final ColorRGBA COL_N = new ColorRGBA(0.90f, 0.20f, 0.20f, 1.00f);
|
||||
private static final ColorRGBA COL_CARD = new ColorRGBA(0.75f, 0.75f, 0.78f, 1.00f);
|
||||
private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.90f, 0.20f, 1.00f);
|
||||
|
||||
// ── Himmelsrichtungen: Name, Winkel-Offset von Nord (Rad) ─────────────────
|
||||
private static final String[] LABELS = {"N", "O", "S", "W"};
|
||||
private static final float[] OFFSETS = {0f, FastMath.HALF_PI, FastMath.PI, -FastMath.HALF_PI};
|
||||
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private BitmapFont font;
|
||||
private Node compassNode;
|
||||
private float cx, cy; // Mittelpunkt (Bildschirmkoordinaten)
|
||||
|
||||
private final BitmapText[] dirLabels = new BitmapText[4];
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
font = assets.loadFont("Interface/Fonts/Default.fnt");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
buildCompass();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
if (compassNode != null) {
|
||||
app.getGuiNode().detachChild(compassNode);
|
||||
compassNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (compassNode == null) { return; }
|
||||
|
||||
Vector3f dir = cam.getDirection();
|
||||
// Yaw vom Weltursprung: 0 = Blick nach +Z (Nord), π/2 = Osten (+X)
|
||||
float yaw = FastMath.atan2(dir.x, dir.z);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// Winkel des Labels im Kompass-Raum (von Oben CW = von +Y Achse)
|
||||
float a = -yaw + OFFSETS[i];
|
||||
float lx = cx + RADIUS * FastMath.sin(a);
|
||||
float ly = cy + RADIUS * FastMath.cos(a);
|
||||
BitmapText lbl = dirLabels[i];
|
||||
lbl.setLocalTranslation(lx - lbl.getLineWidth() * 0.5f, ly + lbl.getLineHeight() * 0.4f, LABEL_Z);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aufbau ───────────────────────────────────────────────────────────────
|
||||
private void buildCompass() {
|
||||
float sw = cam.getWidth();
|
||||
float sh = cam.getHeight();
|
||||
|
||||
// Kompass unten links
|
||||
float ox = MARGIN;
|
||||
// Direkt über der Hotbar: Hotbar.MARGIN_BOT + SLOT_SIZE + Gap
|
||||
float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f;
|
||||
cx = ox + SIZE / 2f;
|
||||
cy = oy + SIZE / 2f;
|
||||
|
||||
compassNode = new Node("compass");
|
||||
|
||||
// Rahmen
|
||||
compassNode.attachChild(makeQuad(ox, oy, SIZE, SIZE, COL_BORDER, 1f));
|
||||
// Hintergrund
|
||||
compassNode.attachChild(makeQuad(ox + 1, oy + 1, SIZE - 2, SIZE - 2, COL_BG, 2f));
|
||||
|
||||
// Vorwärts-Marker: kleine goldene Raute oben in der Mitte
|
||||
compassNode.attachChild(makeQuad(cx - DOT_SZ * 0.5f, cy + RADIUS - DOT_SZ * 0.5f,
|
||||
DOT_SZ, DOT_SZ, COL_MARKER, 3f));
|
||||
|
||||
// Himmelsrichtungs-Labels (Positionen werden in update() gesetzt)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ColorRGBA col = i == 0 ? COL_N : COL_CARD;
|
||||
BitmapText lbl = txt(LABELS[i], 13, col);
|
||||
lbl.setLocalTranslation(cx, cy, LABEL_Z);
|
||||
dirLabels[i] = lbl;
|
||||
compassNode.attachChild(lbl);
|
||||
}
|
||||
|
||||
app.getGuiNode().attachChild(compassNode);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
private Geometry makeQuad(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;
|
||||
}
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA col) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size);
|
||||
t.setColor(col);
|
||||
t.setText(s);
|
||||
t.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class GrassState extends BaseAppState
|
||||
|
||||
private Node grassNode;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private final List<GrassTuft>[] chunkTufts = new List[CHUNK_COUNT];
|
||||
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
|
||||
private final Map<Integer, Material> slotMaterials = new LinkedHashMap<>();
|
||||
|
||||
@@ -63,7 +63,7 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
// ── Samen-Texturen ────────────────────────────────────────────────────────
|
||||
private static final String SEED_TEX_BASE = "Textures/internal/gras/seeds/seeds";
|
||||
private static final String SEED_TEX_EXT = ".png";
|
||||
private static final float SEED_SIZE_FACTOR = 0.45f;
|
||||
private static final float SEED_SIZE_FACTOR = 0.225f;
|
||||
private static final float SEED_Y_FACTOR = 0.78f;
|
||||
|
||||
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||
@@ -75,7 +75,7 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
private Material seedStalkMaterial;
|
||||
private int nextChunk = 0;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
|
||||
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
|
||||
|
||||
@@ -357,7 +357,8 @@ public class GrassVertexRenderState extends BaseAppState
|
||||
float x = b.x();
|
||||
float yBot = b.y() + b.height() * SEED_Y_FACTOR;
|
||||
float z = b.z();
|
||||
float size = b.height() * SEED_SIZE_FACTOR;
|
||||
float seedRand = hash(b.x() * 17.3f, b.z() * 31.9f);
|
||||
float size = b.height() * SEED_SIZE_FACTOR * (0.75f + 0.5f * seedRand);
|
||||
float hw = size * 0.5f;
|
||||
|
||||
// Quad 1 – entlang Welt-X
|
||||
|
||||
261
blight-game/src/main/java/de/blight/game/state/HotbarState.java
Normal file
261
blight-game/src/main/java/de/blight/game/state/HotbarState.java
Normal file
@@ -0,0 +1,261 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.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.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.texture.Texture;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
|
||||
/**
|
||||
* Immer sichtbare Schnellzugriffsleiste (Hotbar) mit 10 Slots (Tasten 1–9, 0).
|
||||
* Items können per Drag & Drop aus dem Inventar in Slots gezogen werden.
|
||||
* Aktiver Slot wird durch eine goldene Umrandung hervorgehoben.
|
||||
*/
|
||||
public class HotbarState extends BaseAppState {
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────
|
||||
static final int SLOT_COUNT = 10;
|
||||
static final float SLOT_SIZE = 52f;
|
||||
static final float SLOT_BORDER = 2f;
|
||||
static final float SLOT_GAP = 4f;
|
||||
static final float SLOT_STRIDE = SLOT_SIZE + SLOT_GAP;
|
||||
static final float THUMB_SZ = SLOT_SIZE - SLOT_BORDER * 2 - 4f;
|
||||
static final float HOTBAR_W = SLOT_COUNT * SLOT_STRIDE - SLOT_GAP;
|
||||
static final float MARGIN_BOT = 16f;
|
||||
|
||||
private static final ColorRGBA COL_BG = new ColorRGBA(0.08f, 0.08f, 0.12f, 0.90f);
|
||||
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.30f, 0.30f, 0.44f, 1.00f);
|
||||
private static final ColorRGBA COL_ACTIVE = new ColorRGBA(0.82f, 0.76f, 0.18f, 1.00f);
|
||||
private static final ColorRGBA COL_KEY = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f);
|
||||
private static final ColorRGBA COL_COUNT = ColorRGBA.White;
|
||||
private static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.18f, 0.18f, 0.26f, 1.00f);
|
||||
|
||||
private static final String[] SLOT_LABEL = {"1","2","3","4","5","6","7","8","9","0"};
|
||||
private static final int[] KEY_CODES = {
|
||||
KeyInput.KEY_1, KeyInput.KEY_2, KeyInput.KEY_3, KeyInput.KEY_4, KeyInput.KEY_5,
|
||||
KeyInput.KEY_6, KeyInput.KEY_7, KeyInput.KEY_8, KeyInput.KEY_9, KeyInput.KEY_0
|
||||
};
|
||||
|
||||
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||
private final MainCharacter mc;
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private BitmapFont font;
|
||||
private Node hotbarNode;
|
||||
private float originX, originY;
|
||||
|
||||
final Item[] slots = new Item[SLOT_COUNT];
|
||||
private int activeSlot = 0;
|
||||
|
||||
private final Geometry[] borderGeoms = new Geometry[SLOT_COUNT];
|
||||
private final Node[] thumbNodes = new Node[SLOT_COUNT];
|
||||
private final BitmapText[] countLabels = new BitmapText[SLOT_COUNT];
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
public HotbarState(MainCharacter mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
assets = app.getAssetManager();
|
||||
font = assets.loadFont("Interface/Fonts/Default.fnt");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
buildHotbar();
|
||||
registerKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
if (hotbarNode != null) {
|
||||
app.getGuiNode().detachChild(hotbarNode);
|
||||
hotbarNode = null;
|
||||
}
|
||||
unregisterKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {}
|
||||
|
||||
// ── Aufbau ───────────────────────────────────────────────────────────────
|
||||
private void buildHotbar() {
|
||||
float sw = app.getCamera().getWidth();
|
||||
float sh = app.getCamera().getHeight();
|
||||
originX = (sw - HOTBAR_W) / 2f;
|
||||
originY = MARGIN_BOT;
|
||||
|
||||
hotbarNode = new Node("hotbar");
|
||||
|
||||
for (int i = 0; i < SLOT_COUNT; i++) {
|
||||
float sx = originX + i * SLOT_STRIDE;
|
||||
float sy = originY;
|
||||
buildSlot(i, sx, sy);
|
||||
}
|
||||
|
||||
app.getGuiNode().attachChild(hotbarNode);
|
||||
}
|
||||
|
||||
private void buildSlot(int idx, float x, float y) {
|
||||
// Rahmen
|
||||
Geometry border = quad(x, y, SLOT_SIZE, SLOT_SIZE,
|
||||
idx == activeSlot ? COL_ACTIVE : COL_BORDER, 1f);
|
||||
borderGeoms[idx] = border;
|
||||
hotbarNode.attachChild(border);
|
||||
|
||||
// Hintergrund
|
||||
hotbarNode.attachChild(quad(x + SLOT_BORDER, y + SLOT_BORDER,
|
||||
SLOT_SIZE - SLOT_BORDER * 2, SLOT_SIZE - SLOT_BORDER * 2, COL_BG, 2f));
|
||||
|
||||
// Thumbnail-Bereich (Node für einfaches Ersetzen)
|
||||
Node tn = new Node("thumb_" + idx);
|
||||
tn.setLocalTranslation(x + SLOT_BORDER + 2f, y + SLOT_BORDER + 2f + 8f, 3f);
|
||||
thumbNodes[idx] = tn;
|
||||
hotbarNode.attachChild(tn);
|
||||
refreshThumb(idx);
|
||||
|
||||
// Taste-Label (oben links)
|
||||
BitmapText keyLbl = txt(SLOT_LABEL[idx], 11, COL_KEY);
|
||||
keyLbl.setLocalTranslation(x + 3f, y + SLOT_SIZE - 2f, 4f);
|
||||
hotbarNode.attachChild(keyLbl);
|
||||
|
||||
// Anzahl-Label (unten rechts)
|
||||
BitmapText cntLbl = txt("", 11, COL_COUNT);
|
||||
cntLbl.setLocalTranslation(x + SLOT_SIZE - 20f, y + 14f, 4f);
|
||||
countLabels[idx] = cntLbl;
|
||||
hotbarNode.attachChild(cntLbl);
|
||||
}
|
||||
|
||||
// ── Thumb aktualisieren ───────────────────────────────────────────────────
|
||||
private void refreshThumb(int idx) {
|
||||
Node tn = thumbNodes[idx];
|
||||
tn.detachAllChildren();
|
||||
|
||||
Item item = slots[idx];
|
||||
if (item == null) {
|
||||
tn.attachChild(quad(0, 0, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, 0f));
|
||||
} else {
|
||||
Texture tex = loadThumb(item);
|
||||
if (tex != null) {
|
||||
Geometry g = new Geometry("t", new Quad(THUMB_SZ, THUMB_SZ));
|
||||
Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
m.setTexture("ColorMap", tex);
|
||||
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
g.setMaterial(m);
|
||||
g.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
tn.attachChild(g);
|
||||
} else {
|
||||
tn.attachChild(quad(0, 0, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, 0f));
|
||||
}
|
||||
// Anzahl aus Inventar
|
||||
int count = mc.getInventar() != null
|
||||
? mc.getInventar().getItems().getOrDefault(item, 0) : 0;
|
||||
countLabels[idx].setText(count > 1 ? String.valueOf(count) : "");
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshBorder(int idx) {
|
||||
borderGeoms[idx].getMaterial().setColor("Color",
|
||||
idx == activeSlot ? COL_ACTIVE.clone() : COL_BORDER.clone());
|
||||
}
|
||||
|
||||
// ── Drop-API für InventoryState ───────────────────────────────────────────
|
||||
/**
|
||||
* Nimmt ein per Drag & Drop abgelegtes Item an, sofern {@code screenPos}
|
||||
* auf einen Slot zeigt. Gibt true zurück wenn der Drop angenommen wurde.
|
||||
*/
|
||||
public boolean tryDrop(Vector2f screenPos, Item item) {
|
||||
for (int i = 0; i < SLOT_COUNT; i++) {
|
||||
float sx = originX + i * SLOT_STRIDE;
|
||||
if (screenPos.x >= sx && screenPos.x <= sx + SLOT_SIZE
|
||||
&& screenPos.y >= originY && screenPos.y <= originY + SLOT_SIZE) {
|
||||
slots[i] = item;
|
||||
refreshThumb(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Tasten ────────────────────────────────────────────────────────────────
|
||||
private void registerKeys() {
|
||||
for (int i = 0; i < SLOT_COUNT; i++) {
|
||||
String m = "_Hotbar" + i;
|
||||
app.getInputManager().addMapping(m, new KeyTrigger(KEY_CODES[i]));
|
||||
final int slot = i;
|
||||
app.getInputManager().addListener(
|
||||
(ActionListener) (name, pressed, tpf) -> { if (pressed) selectSlot(slot); }, m);
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterKeys() {
|
||||
for (int i = 0; i < SLOT_COUNT; i++) {
|
||||
try { app.getInputManager().deleteMapping("_Hotbar" + i); }
|
||||
catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void selectSlot(int idx) {
|
||||
int prev = activeSlot;
|
||||
activeSlot = idx;
|
||||
refreshBorder(prev);
|
||||
refreshBorder(idx);
|
||||
// Item benutzen wenn Slot belegt und konsumierbar
|
||||
Item item = slots[idx];
|
||||
if (item != null && item.isConsumable() && mc != null) {
|
||||
item.use(mc);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
private Texture loadThumb(Item item) {
|
||||
if (item.getModelRef() == null) return null;
|
||||
String tp = item.getModelRef().getThumbnailAssetPath();
|
||||
if (tp == null) return null;
|
||||
try { return assets.loadTexture(tp); } catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
private Geometry quad(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;
|
||||
}
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA col) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size);
|
||||
t.setColor(col);
|
||||
t.setText(s);
|
||||
t.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.font.BitmapFont;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
@@ -20,14 +16,13 @@ import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.post.FilterPostProcessor;
|
||||
import com.jme3.texture.Texture;
|
||||
import de.blight.common.model.*;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
import de.blight.game.config.MenuCanvas;
|
||||
import de.blight.game.config.NinePatch;
|
||||
import de.blight.game.post.GaussianBlurFilter;
|
||||
import de.blight.game.scene.WorldScene;
|
||||
import de.blight.game.config.OverlayState;
|
||||
import de.blight.game.config.UiTheme;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -39,24 +34,14 @@ import java.util.stream.Collectors;
|
||||
* SubKategorie und dann nach Preis sortiert angezeigt.
|
||||
* Thumbnails werden aus {@code ObjectReference.getThumbnailAssetPath()} geladen.
|
||||
*/
|
||||
public class InventoryState extends BaseAppState {
|
||||
public class InventoryState extends OverlayState {
|
||||
|
||||
// ── Farben ────────────────────────────────────────────────────────────────
|
||||
// ── Inventar-spezifische Farben ───────────────────────────────────────────
|
||||
|
||||
private static final ColorRGBA COL_OVERLAY = new ColorRGBA(0f, 0f, 0f, 0.72f);
|
||||
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.09f, 0.09f, 0.14f, 0.97f);
|
||||
private static final ColorRGBA COL_HDR = new ColorRGBA(0.05f, 0.05f, 0.09f, 1.00f);
|
||||
private static final ColorRGBA COL_TAB_ON = new ColorRGBA(0.22f, 0.22f, 0.42f, 1.00f);
|
||||
private static final ColorRGBA COL_TAB_OFF = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f);
|
||||
private static final ColorRGBA COL_CELL = new ColorRGBA(0.14f, 0.14f, 0.22f, 1.00f);
|
||||
private static final ColorRGBA COL_CELL_FRAME = new ColorRGBA(0.22f, 0.22f, 0.35f, 1.00f);
|
||||
private static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.20f, 0.20f, 0.28f, 1.00f);
|
||||
private static final ColorRGBA COL_BADGE = new ColorRGBA(0.04f, 0.04f, 0.07f, 0.92f);
|
||||
private static final ColorRGBA COL_SUBHDR = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.80f);
|
||||
private static final ColorRGBA COL_WHITE = ColorRGBA.White;
|
||||
private static final ColorRGBA COL_MUTED = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f);
|
||||
private static final ColorRGBA COL_GOLD = new ColorRGBA(1.00f, 0.85f, 0.20f, 1.00f);
|
||||
private static final ColorRGBA COL_SUBCAT_TXT = new ColorRGBA(0.55f, 0.78f, 1.00f, 1.00f);
|
||||
private static final ColorRGBA COL_THUMB_BG = UiTheme.COL_THUMB_BG;
|
||||
private static final ColorRGBA COL_BADGE = UiTheme.COL_BADGE;
|
||||
private static final ColorRGBA COL_SUBHDR = UiTheme.COL_SUBHDR;
|
||||
private static final ColorRGBA COL_SUBCAT_TXT = UiTheme.COL_SUBCAT_TXT;
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -65,9 +50,7 @@ public class InventoryState extends BaseAppState {
|
||||
private static final int CELL_H = 195; // 128 thumb + 67 text
|
||||
private static final int CELL_GAP = 8;
|
||||
private static final int THUMB_SZ = 128;
|
||||
private static final int HDR_H = 42;
|
||||
private static final int TAB_H = 32;
|
||||
private static final int PAD = 16; // panel inner padding
|
||||
private static final int SUBHDR_H = 26;
|
||||
private static final int SUBHDR_GAP = 4;
|
||||
|
||||
@@ -80,15 +63,8 @@ public class InventoryState extends BaseAppState {
|
||||
|
||||
// ── JME-Zustand ───────────────────────────────────────────────────────────
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assetManager;
|
||||
private BitmapFont font;
|
||||
private Node guiNode;
|
||||
private Node panel;
|
||||
private Node gridNode;
|
||||
private Node bgLayer;
|
||||
private Node canvasNode;
|
||||
private GaussianBlurFilter blurFilter;
|
||||
|
||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -98,14 +74,15 @@ public class InventoryState extends BaseAppState {
|
||||
// ── UI-Zustand ────────────────────────────────────────────────────────────
|
||||
|
||||
private ItemCategory activeTab = null;
|
||||
private float scrollY = 0f; // Pixel-Scroll-Offset (nach unten = positiv)
|
||||
private float scrollY = 0f;
|
||||
private float maxScrollY = 0f;
|
||||
private float uiScale = 1f; // gespeichert aus buildContent
|
||||
|
||||
// Für Tab-Klick-Erkennung: parallele Arrays
|
||||
private ItemCategory[] tabOrder;
|
||||
private float[][] tabBounds; // [tabIdx] = {x, y, w, h}
|
||||
private float[][] tabBounds;
|
||||
|
||||
// Content-Bereich in Screen-Koordinaten (JME3 Y-up)
|
||||
// Content-Bereich in virtuellen Koordinaten
|
||||
private float contentLeft;
|
||||
private float contentBottom;
|
||||
private float contentTop;
|
||||
@@ -120,15 +97,15 @@ public class InventoryState extends BaseAppState {
|
||||
this.keyBindings = keyBindings;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
// ── OverlayState-Implementierung ──────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assetManager = app.getAssetManager();
|
||||
this.guiNode = this.app.getGuiNode();
|
||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
protected String getTitle() {
|
||||
return "Inventar";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initOverlayKeys() {
|
||||
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(keyBindings.inventory));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_DN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
@@ -137,63 +114,65 @@ public class InventoryState extends BaseAppState {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
scrollY = 0f;
|
||||
buildPanel();
|
||||
app.getInputManager().setCursorVisible(true);
|
||||
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
|
||||
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
destroyPanel();
|
||||
app.getInputManager().removeListener(scrollListener);
|
||||
app.getInputManager().removeListener(clickListener);
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
protected void cleanupOverlayKeys() {
|
||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_DN);
|
||||
app.getInputManager().deleteMapping(MAP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildContent(Node parentPanel, float scale) {
|
||||
this.uiScale = scale;
|
||||
this.panel = parentPanel;
|
||||
|
||||
// Content-Bereich (für Scroll-Berechnungen), innerhalb des OverlayState-Panels
|
||||
contentLeft = PX + PAD;
|
||||
contentBottom = PY + PAD;
|
||||
contentTop = PY + PH - HDR_H - TAB_H - 8;
|
||||
|
||||
buildTabs(parentPanel);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
scrollY = 0f;
|
||||
super.onEnable();
|
||||
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
|
||||
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
app.getInputManager().removeListener(scrollListener);
|
||||
app.getInputManager().removeListener(clickListener);
|
||||
panel = null;
|
||||
gridNode = null;
|
||||
super.onDisable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
super.cleanup(app);
|
||||
}
|
||||
|
||||
// ── Listener ──────────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener toggleListener = (name, pressed, tpf) -> {
|
||||
if (pressed && MAP_TOGGLE.equals(name)) setEnabled(!isEnabled());
|
||||
if (pressed && MAP_TOGGLE.equals(name)) { setEnabled(!isEnabled()); }
|
||||
};
|
||||
|
||||
private final AnalogListener scrollListener = (name, value, tpf) -> {
|
||||
float step = 60f * value;
|
||||
if (MAP_SCROLL_UP.equals(name)) scrollY = Math.max(0f, scrollY - step);
|
||||
else scrollY = Math.min(maxScrollY, scrollY + step);
|
||||
if (MAP_SCROLL_UP.equals(name)) { scrollY = Math.max(0f, scrollY - step); }
|
||||
else { scrollY = Math.min(maxScrollY, scrollY + step); }
|
||||
rebuildGrid();
|
||||
};
|
||||
|
||||
private final ActionListener clickListener = (name, pressed, tpf) -> {
|
||||
if (!pressed || panel == null || tabBounds == null) return;
|
||||
if (!pressed || panel == null || tabBounds == null) { return; }
|
||||
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||
for (int i = 0; i < tabBounds.length; i++) {
|
||||
float[] b = tabBounds[i];
|
||||
@@ -204,60 +183,27 @@ public class InventoryState extends BaseAppState {
|
||||
}
|
||||
};
|
||||
|
||||
// ── Haupt-Panel aufbauen ──────────────────────────────────────────────────
|
||||
// ── Tabs aufbauen ─────────────────────────────────────────────────────────
|
||||
|
||||
private void buildPanel() {
|
||||
float sw = MenuCanvas.REF_W;
|
||||
float sh = MenuCanvas.REF_H;
|
||||
|
||||
bgLayer = MenuCanvas.createBgLayer(assetManager, app.getCamera());
|
||||
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||
guiNode.attachChild(bgLayer);
|
||||
guiNode.attachChild(canvasNode);
|
||||
|
||||
// Panelgröße: 5 Spalten + Ränder
|
||||
float pw = COLS * (CELL_W + CELL_GAP) + CELL_GAP + 2 * PAD;
|
||||
float ph = HDR_H + TAB_H + 4 + 450 + PAD; // header + tabs + gap + content + bottom
|
||||
float px = (sw - pw) / 2f;
|
||||
float py = (sh - ph) / 2f;
|
||||
|
||||
panel = new Node("inv-panel");
|
||||
panel.attachChild(NinePatch.panel(assetManager).build(px, py, pw, ph, -19));
|
||||
quad(panel, px, py + ph - HDR_H, pw, HDR_H, COL_HDR, -18); // Header-Balken
|
||||
|
||||
// Titel
|
||||
BitmapText title = txt("Inventar", 22, COL_WHITE);
|
||||
title.setLocalTranslation(px + PAD, py + ph - 12, -17);
|
||||
panel.attachChild(title);
|
||||
|
||||
// Content-Bereich (für Scroll-Berechnungen)
|
||||
contentLeft = px + PAD;
|
||||
contentBottom = py + PAD;
|
||||
contentTop = py + ph - HDR_H - TAB_H - 8;
|
||||
|
||||
// Tabs aufbauen
|
||||
buildTabs(px, py, pw, ph);
|
||||
|
||||
canvasNode.attachChild(panel);
|
||||
}
|
||||
|
||||
private void buildTabs(float px, float py, float pw, float ph) {
|
||||
private void buildTabs(Node panel) {
|
||||
tabOrder = ItemCategory.values();
|
||||
tabBounds = new float[tabOrder.length][4];
|
||||
|
||||
if (activeTab == null) activeTab = tabOrder[0];
|
||||
if (activeTab == null) { activeTab = tabOrder[0]; }
|
||||
|
||||
float tabY = py + ph - HDR_H - TAB_H;
|
||||
float tabW = (pw - 8) / tabOrder.length;
|
||||
float tabX0 = px + 4;
|
||||
float tabY = PY + PH - HDR_H - TAB_H;
|
||||
float tabW = (PW - 8) / tabOrder.length;
|
||||
float tabX0 = PX + 4;
|
||||
|
||||
for (int i = 0; i < tabOrder.length; i++) {
|
||||
boolean active = tabOrder[i] == activeTab;
|
||||
float tx = tabX0 + i * tabW;
|
||||
NinePatch tabPatch = active ? NinePatch.tabActive(assetManager) : NinePatch.tabInactive(assetManager);
|
||||
panel.attachChild(tabPatch.build(tx, tabY, tabW - 4, TAB_H, -18));
|
||||
BitmapText lbl = txt(catLabel(tabOrder[i]), 13, active ? COL_WHITE : COL_MUTED);
|
||||
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
|
||||
NinePatch tabPatch = active ? NinePatch.tabActive(assets) : NinePatch.tabInactive(assets);
|
||||
panel.attachChild(tabPatch.build(tx, tabY, tabW - 4, TAB_H, -18f));
|
||||
BitmapText lbl = crispText(catLabel(tabOrder[i]), UiTheme.FONT_SMALL,
|
||||
active ? UiTheme.COL_WHITE : UiTheme.COL_MUTED);
|
||||
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f,
|
||||
tabY + TAB_H - 8, -17);
|
||||
panel.attachChild(lbl);
|
||||
tabBounds[i] = new float[]{ tx, tabY, tabW - 4, TAB_H };
|
||||
}
|
||||
@@ -269,23 +215,22 @@ public class InventoryState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void switchTab(ItemCategory cat) {
|
||||
if (cat == activeTab) return;
|
||||
if (cat == activeTab) { return; }
|
||||
activeTab = cat;
|
||||
scrollY = 0f;
|
||||
activeItems = sortedItems(cat);
|
||||
destroyPanel();
|
||||
buildPanel();
|
||||
setEnabled(false);
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
// ── Item-Grid ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Erstellt den Grid-Node und hängt ihn ans Panel. */
|
||||
private void buildGrid() {
|
||||
gridNode = new Node("inv-grid");
|
||||
panel.attachChild(gridNode);
|
||||
|
||||
if (activeItems.isEmpty()) {
|
||||
BitmapText empty = txt("Keine Items vorhanden", 15, COL_MUTED);
|
||||
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
|
||||
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
|
||||
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
|
||||
empty.setLocalTranslation(ex, ey, -17);
|
||||
@@ -293,38 +238,33 @@ public class InventoryState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Items nach SubKategorie gruppiert (Reihenfolge aus sortedItems beibehalten)
|
||||
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
|
||||
for (Map.Entry<Item, Integer> e : activeItems) {
|
||||
groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e);
|
||||
}
|
||||
|
||||
// Virtual Y: beginnt am Top des Content-Bereichs, läuft nach unten (Y nimmt ab)
|
||||
float virtY = contentTop - scrollY;
|
||||
|
||||
for (Map.Entry<ItemSubCategory, List<Map.Entry<Item, Integer>>> group : groups.entrySet()) {
|
||||
// SubKategorie-Header
|
||||
float subHdrY = virtY - SUBHDR_H;
|
||||
if (subHdrY + SUBHDR_H >= contentBottom && subHdrY <= contentTop) {
|
||||
String subLabel = group.getKey() != null ? subCatLabel(group.getKey()) : "Sonstiges";
|
||||
float subBgW = COLS * (CELL_W + CELL_GAP) - CELL_GAP;
|
||||
quad(gridNode, contentLeft, subHdrY, subBgW, SUBHDR_H, COL_SUBHDR, -18);
|
||||
BitmapText sLbl = txt(" " + subLabel, 12, COL_SUBCAT_TXT);
|
||||
addQuad(gridNode, contentLeft, subHdrY, subBgW, SUBHDR_H, COL_SUBHDR, -18f);
|
||||
BitmapText sLbl = crispText(" " + subLabel, UiTheme.FONT_BODY, COL_SUBCAT_TXT);
|
||||
sLbl.setLocalTranslation(contentLeft + 6, subHdrY + SUBHDR_H - 7, -17);
|
||||
gridNode.attachChild(sLbl);
|
||||
}
|
||||
virtY -= SUBHDR_H + SUBHDR_GAP;
|
||||
|
||||
// Item-Zellen zeilenweise
|
||||
List<Map.Entry<Item, Integer>> groupItems = group.getValue();
|
||||
int col = 0;
|
||||
for (Map.Entry<Item, Integer> entry : groupItems) {
|
||||
if (col == 0) virtY -= CELL_H;
|
||||
if (col == 0) { virtY -= CELL_H; }
|
||||
|
||||
float cellX = contentLeft + col * (CELL_W + CELL_GAP);
|
||||
float cellY = virtY;
|
||||
|
||||
// Nur rendern wenn vollständig im sichtbaren Content-Bereich
|
||||
if (cellY >= contentBottom && cellY + CELL_H <= contentTop) {
|
||||
buildCell(gridNode, entry.getKey(), entry.getValue(), cellX, cellY);
|
||||
}
|
||||
@@ -335,20 +275,17 @@ public class InventoryState extends BaseAppState {
|
||||
virtY -= CELL_GAP;
|
||||
}
|
||||
}
|
||||
// Wenn letzte Zeile nicht voll war, Y-Schritt nachholen
|
||||
if (col != 0) virtY -= CELL_GAP;
|
||||
virtY -= CELL_GAP; // Abstand zwischen Gruppen
|
||||
if (col != 0) { virtY -= CELL_GAP; }
|
||||
virtY -= CELL_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
/** Entfernt den Grid-Node und erstellt ihn neu (bei Scroll). */
|
||||
private void rebuildGrid() {
|
||||
if (panel == null) return;
|
||||
if (gridNode != null) panel.detachChild(gridNode);
|
||||
if (panel == null) { return; }
|
||||
if (gridNode != null) { panel.detachChild(gridNode); }
|
||||
buildGrid();
|
||||
}
|
||||
|
||||
/** Berechnet maximalen Scroll-Offset in Pixel. */
|
||||
private float computeMaxScroll() {
|
||||
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
|
||||
for (Map.Entry<Item, Integer> e : activeItems) {
|
||||
@@ -367,67 +304,61 @@ public class InventoryState extends BaseAppState {
|
||||
// ── Einzel-Zelle ──────────────────────────────────────────────────────────
|
||||
|
||||
private void buildCell(Node parent, Item item, int count, float x, float y) {
|
||||
parent.attachChild(NinePatch.itemCell(assetManager).build(x, y, CELL_W, CELL_H, -18));
|
||||
parent.attachChild(NinePatch.itemCell(assets).build(x, y, CELL_W, CELL_H, -18f));
|
||||
|
||||
// Thumbnail
|
||||
float thumbX = x + (CELL_W - THUMB_SZ) / 2f;
|
||||
float thumbY = y + CELL_H - THUMB_SZ - 6;
|
||||
|
||||
Texture thumb = null;
|
||||
if (item.getModelRef() != null) {
|
||||
String tp = item.getModelRef().getThumbnailAssetPath();
|
||||
if (tp != null) thumb = loadThumb(tp);
|
||||
if (tp != null) { thumb = loadThumb(tp); }
|
||||
}
|
||||
|
||||
if (thumb != null) {
|
||||
Geometry tg = new Geometry("thumb", new Quad(THUMB_SZ, THUMB_SZ));
|
||||
Material tm = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
Material tm = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
tm.setTexture("ColorMap", thumb);
|
||||
tm.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
tg.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
tg.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
tg.setMaterial(tm);
|
||||
tg.setLocalTranslation(thumbX, thumbY, -16);
|
||||
tg.setLocalTranslation(thumbX, thumbY, -16f);
|
||||
parent.attachChild(tg);
|
||||
} else {
|
||||
quad(parent, thumbX, thumbY, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, -16);
|
||||
addQuad(parent, thumbX, thumbY, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, -16);
|
||||
}
|
||||
|
||||
// Anzahl-Badge unten rechts (nur wenn > 1)
|
||||
if (count > 1) {
|
||||
String cntStr = count > 999 ? "999+" : String.valueOf(count);
|
||||
BitmapText cntTxt = txt(cntStr, 13, COL_WHITE);
|
||||
BitmapText cntTxt = crispText(cntStr, UiTheme.FONT_SMALL, UiTheme.COL_WHITE);
|
||||
float bw = Math.max(26, cntTxt.getLineWidth() + 8);
|
||||
float bh = 18;
|
||||
float bx = x + CELL_W - bw - 5;
|
||||
float by = thumbY + 4;
|
||||
quad(parent, bx, by, bw, bh, COL_BADGE, -15);
|
||||
addQuad(parent, bx, by, bw, bh, COL_BADGE, -15f);
|
||||
cntTxt.setLocalTranslation(bx + (bw - cntTxt.getLineWidth()) / 2f, by + bh - 3, -14);
|
||||
parent.attachChild(cntTxt);
|
||||
}
|
||||
|
||||
// Name
|
||||
BitmapText nameTxt = txt(clip(item.getDisplayText(), 17), 13, COL_WHITE);
|
||||
BitmapText nameTxt = crispText(clip(item.getDisplayText(), 17), UiTheme.FONT_SMALL, UiTheme.COL_WHITE);
|
||||
nameTxt.setLocalTranslation(x + (CELL_W - nameTxt.getLineWidth()) / 2f, y + 42, -16);
|
||||
parent.attachChild(nameTxt);
|
||||
|
||||
// Goldwert
|
||||
BitmapText goldTxt = txt(item.getWorthGold() + " G", 12, COL_GOLD);
|
||||
BitmapText goldTxt = crispText(item.getWorthGold() + " G", UiTheme.FONT_BODY, UiTheme.COL_GOLD);
|
||||
goldTxt.setLocalTranslation(x + (CELL_W - goldTxt.getLineWidth()) / 2f, y + 22, -16);
|
||||
parent.attachChild(goldTxt);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Lädt Texture oder gibt null zurück wenn nicht vorhanden. */
|
||||
private Texture loadThumb(String assetPath) {
|
||||
try { return assetManager.loadTexture(assetPath); }
|
||||
try { return assets.loadTexture(assetPath); }
|
||||
catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
/** Items des Tabs sortiert nach SubKategorie-Ordinal, dann Preis. */
|
||||
private List<Map.Entry<Item, Integer>> sortedItems(ItemCategory cat) {
|
||||
Inventar inv = mc.getInventar();
|
||||
if (inv == null) return List.of();
|
||||
if (inv == null) { return List.of(); }
|
||||
return inv.getItems().entrySet().stream()
|
||||
.filter(e -> e.getKey().getCategory() == cat)
|
||||
.sorted(Comparator
|
||||
@@ -439,40 +370,6 @@ public class InventoryState extends BaseAppState {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private float[] toVirtual(Vector2f screen) {
|
||||
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||
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 };
|
||||
}
|
||||
|
||||
private void destroyPanel() {
|
||||
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||
panel = null; gridNode = null;
|
||||
}
|
||||
|
||||
private Geometry quad(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(assetManager, "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);
|
||||
}
|
||||
g.setMaterial(m);
|
||||
g.setLocalTranslation(x, y, z);
|
||||
parent.attachChild(g);
|
||||
return g;
|
||||
}
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA col) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size); t.setColor(col); t.setText(s);
|
||||
return t;
|
||||
}
|
||||
|
||||
private static String clip(String s, int max) {
|
||||
return s.length() <= max ? s : s.substring(0, max - 1) + "…";
|
||||
}
|
||||
|
||||
462
blight-game/src/main/java/de/blight/game/state/QuestState.java
Normal file
462
blight-game/src/main/java/de/blight/game/state/QuestState.java
Normal file
@@ -0,0 +1,462 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.font.BitmapText;
|
||||
import com.jme3.input.KeyInput;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.AnalogListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseAxisTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.TextRegistry;
|
||||
import de.blight.common.model.quests.*;
|
||||
import de.blight.game.config.NinePatch;
|
||||
import de.blight.game.config.OverlayState;
|
||||
import de.blight.game.config.UiTheme;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quest-Übersicht: offene Quests (links Liste, rechts Details) mit Tab für abgeschlossene.
|
||||
* Öffnet/schließt mit Taste V oder ESC.
|
||||
*/
|
||||
public class QuestState extends OverlayState {
|
||||
|
||||
// ── Layout (innerhalb des OverlayState-Panels PX/PY/PW/PH) ──────────────
|
||||
|
||||
private static final float TAB_H = 32f;
|
||||
private static final float LIST_W = 270f;
|
||||
private static final float ITEM_H = 42f;
|
||||
private static final float ITEM_GAP = 3f;
|
||||
private static final float DETAIL_X = PX + PAD + LIST_W + 12f;
|
||||
private static final float DETAIL_W = PW - PAD - LIST_W - 12f - PAD;
|
||||
private static final float CONTENT_TOP = PY + PH - HDR_H - TAB_H - PAD;
|
||||
private static final float CONTENT_BOTTOM = PY + PAD;
|
||||
private static final float CONTENT_H = CONTENT_TOP - CONTENT_BOTTOM;
|
||||
private static final int WRAP_CHARS = 48;
|
||||
|
||||
// ── Input-Mappings ────────────────────────────────────────────────────────
|
||||
|
||||
private static final String MAP_TOGGLE = "_QuestToggle";
|
||||
private static final String MAP_SCROLL_UP = "_QuestScrollUp";
|
||||
private static final String MAP_SCROLL_DN = "_QuestScrollDn";
|
||||
private static final String MAP_CLICK = "_QuestClick";
|
||||
|
||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private final MainCharacter mc;
|
||||
|
||||
// ── UI-Zustand ────────────────────────────────────────────────────────────
|
||||
|
||||
private Node panel;
|
||||
private Node listNode;
|
||||
private Node detailNode;
|
||||
|
||||
private boolean showCompleted = false;
|
||||
private int selectedIdx = 0;
|
||||
private int scrollOffset = 0;
|
||||
|
||||
private float[][] tabBounds = new float[2][4];
|
||||
private final List<float[]> itemBounds = new ArrayList<>();
|
||||
|
||||
public QuestState(MainCharacter mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
// ── OverlayState-Implementierung ──────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return "Questlog";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initOverlayKeys() {
|
||||
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(KeyInput.KEY_V));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_DN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
app.getInputManager().addMapping(MAP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
app.getInputManager().addListener(toggleListener, MAP_TOGGLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanupOverlayKeys() {
|
||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_DN);
|
||||
app.getInputManager().deleteMapping(MAP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildContent(Node parentPanel, float scale) {
|
||||
this.panel = parentPanel;
|
||||
|
||||
// Tabs
|
||||
buildTabs(parentPanel);
|
||||
|
||||
// Trennlinie Liste / Detail
|
||||
float divX = PX + PAD + LIST_W + 6f;
|
||||
addQuad(parentPanel, divX, CONTENT_BOTTOM, 1f, CONTENT_H, UiTheme.COL_DIVIDER, -17f);
|
||||
|
||||
listNode = new Node("quest-list");
|
||||
detailNode = new Node("quest-detail");
|
||||
parentPanel.attachChild(listNode);
|
||||
parentPanel.attachChild(detailNode);
|
||||
|
||||
buildList();
|
||||
buildDetail();
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
selectedIdx = 0;
|
||||
scrollOffset = 0;
|
||||
super.onEnable();
|
||||
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
|
||||
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
app.getInputManager().removeListener(scrollListener);
|
||||
app.getInputManager().removeListener(clickListener);
|
||||
panel = null;
|
||||
listNode = null;
|
||||
detailNode = null;
|
||||
super.onDisable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
super.cleanup(application);
|
||||
}
|
||||
|
||||
// ── Listener ──────────────────────────────────────────────────────────────
|
||||
|
||||
private final ActionListener toggleListener = (name, pressed, tpf) -> {
|
||||
if (pressed) { setEnabled(!isEnabled()); }
|
||||
};
|
||||
|
||||
private final AnalogListener scrollListener = (name, value, tpf) -> {
|
||||
List<Quest> quests = activeList();
|
||||
int maxScroll = Math.max(0, quests.size() - visibleSlots());
|
||||
if (MAP_SCROLL_UP.equals(name)) { scrollOffset = Math.max(0, scrollOffset - 1); }
|
||||
else { scrollOffset = Math.min(maxScroll, scrollOffset + 1); }
|
||||
rebuildList();
|
||||
};
|
||||
|
||||
private final ActionListener clickListener = (name, pressed, tpf) -> {
|
||||
if (!pressed || panel == null) { return; }
|
||||
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
float[] b = tabBounds[i];
|
||||
if (v[0] >= b[0] && v[0] <= b[0]+b[2] && v[1] >= b[1] && v[1] <= b[1]+b[3]) {
|
||||
boolean wantCompleted = (i == 1);
|
||||
if (wantCompleted != showCompleted) {
|
||||
showCompleted = wantCompleted;
|
||||
selectedIdx = 0;
|
||||
scrollOffset = 0;
|
||||
rebuild();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < itemBounds.size(); i++) {
|
||||
float[] b = itemBounds.get(i);
|
||||
if (v[0] >= b[0] && v[0] <= b[0]+b[2] && v[1] >= b[1] && v[1] <= b[1]+b[3]) {
|
||||
int newIdx = scrollOffset + i;
|
||||
List<Quest> quests = activeList();
|
||||
if (newIdx < quests.size() && newIdx != selectedIdx) {
|
||||
selectedIdx = newIdx;
|
||||
rebuildList();
|
||||
rebuildDetail();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── Tabs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildTabs(Node panel) {
|
||||
float tabY = PY + PH - HDR_H - TAB_H;
|
||||
float tabW = (PW - 8f) / 2f;
|
||||
float tabX0 = PX + 4f;
|
||||
String[] labels = {"Offene Quests", "Abgeschlossen"};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
boolean active = (i == 0 && !showCompleted) || (i == 1 && showCompleted);
|
||||
float tx = tabX0 + i * tabW;
|
||||
NinePatch patch = active ? NinePatch.tabActive(assets) : NinePatch.tabInactive(assets);
|
||||
panel.attachChild(patch.build(tx, tabY, tabW - 4, TAB_H, -18f));
|
||||
BitmapText lbl = crispText(labels[i], UiTheme.FONT_SMALL,
|
||||
active ? UiTheme.COL_WHITE : UiTheme.COL_MUTED);
|
||||
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
|
||||
panel.attachChild(lbl);
|
||||
tabBounds[i] = new float[]{tx, tabY, tabW - 4, TAB_H};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Liste ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildList() {
|
||||
itemBounds.clear();
|
||||
List<Quest> quests = activeList();
|
||||
float x = PX + PAD;
|
||||
|
||||
if (quests.isEmpty()) {
|
||||
BitmapText empty = crispText(
|
||||
showCompleted ? "Keine abgeschlossenen Quests." : "Keine offenen Quests.",
|
||||
UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
empty.setLocalTranslation(x, CONTENT_TOP - 20, -17);
|
||||
listNode.attachChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
int slots = visibleSlots();
|
||||
int end = Math.min(scrollOffset + slots, quests.size());
|
||||
|
||||
for (int i = scrollOffset; i < end; i++) {
|
||||
Quest q = quests.get(i);
|
||||
int slot = i - scrollOffset;
|
||||
float iy = CONTENT_TOP - slot * (ITEM_H + ITEM_GAP) - ITEM_H;
|
||||
|
||||
boolean sel = (i == selectedIdx);
|
||||
ColorRGBA bg = sel ? UiTheme.COL_SELECTED : UiTheme.COL_ITEM_BG;
|
||||
addQuad(listNode, x, iy, LIST_W, ITEM_H, bg, -18);
|
||||
|
||||
String typeLabel = typeLabel(q);
|
||||
ColorRGBA typeCol = typeColor(q);
|
||||
BitmapText typeTxt = crispText(typeLabel, UiTheme.FONT_TINY, typeCol);
|
||||
typeTxt.setLocalTranslation(x + 4, iy + ITEM_H - 5, -17);
|
||||
listNode.attachChild(typeTxt);
|
||||
|
||||
String title = resolveTitle(q);
|
||||
if (title.length() > 26) { title = title.substring(0, 25) + "…"; }
|
||||
BitmapText titleTxt = crispText(title, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
|
||||
titleTxt.setLocalTranslation(x + 4, iy + ITEM_H - 18, -17);
|
||||
listNode.attachChild(titleTxt);
|
||||
|
||||
if (q.getXp() > 0) {
|
||||
BitmapText xpTxt = crispText(q.getXp() + " XP", UiTheme.FONT_TINY,
|
||||
showCompleted ? UiTheme.COL_SUCCESS : UiTheme.COL_XP);
|
||||
xpTxt.setLocalTranslation(x + LIST_W - xpTxt.getLineWidth() - 6, iy + 6, -17);
|
||||
listNode.attachChild(xpTxt);
|
||||
}
|
||||
|
||||
itemBounds.add(new float[]{x, iy, LIST_W, ITEM_H});
|
||||
}
|
||||
|
||||
if (scrollOffset > 0) {
|
||||
BitmapText up = crispText("▲", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
up.setLocalTranslation(x + LIST_W / 2f - 5, CONTENT_TOP + 2, -17);
|
||||
listNode.attachChild(up);
|
||||
}
|
||||
if (scrollOffset + slots < quests.size()) {
|
||||
BitmapText dn = crispText("▼", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
dn.setLocalTranslation(x + LIST_W / 2f - 5, CONTENT_BOTTOM - 2, -17);
|
||||
listNode.attachChild(dn);
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildList() {
|
||||
listNode.detachAllChildren();
|
||||
itemBounds.clear();
|
||||
buildList();
|
||||
}
|
||||
|
||||
// ── Detail ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildDetail() {
|
||||
List<Quest> quests = activeList();
|
||||
float x = DETAIL_X;
|
||||
float y = CONTENT_TOP;
|
||||
float w = DETAIL_W;
|
||||
|
||||
if (quests.isEmpty() || selectedIdx >= quests.size()) {
|
||||
BitmapText nq = crispText("Keinen Quest ausgewählt.", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
nq.setLocalTranslation(x, y - 20, -17);
|
||||
detailNode.attachChild(nq);
|
||||
return;
|
||||
}
|
||||
|
||||
Quest q = quests.get(selectedIdx);
|
||||
|
||||
String title = resolveTitle(q);
|
||||
BitmapText titleTxt = crispText(title, UiTheme.FONT_HEADING, UiTheme.COL_GOLD);
|
||||
titleTxt.setLocalTranslation(x, y, -17);
|
||||
detailNode.attachChild(titleTxt);
|
||||
y -= 24;
|
||||
|
||||
String typeLabel = typeLabel(q);
|
||||
BitmapText typeTxt = crispText("[" + typeLabel + "]", UiTheme.FONT_BODY, typeColor(q));
|
||||
typeTxt.setLocalTranslation(x, y, -17);
|
||||
detailNode.attachChild(typeTxt);
|
||||
if (q.getXp() > 0) {
|
||||
ColorRGBA xpCol = showCompleted ? UiTheme.COL_SUCCESS : UiTheme.COL_XP;
|
||||
BitmapText xpTxt = crispText("Belohnung: " + q.getXp() + " XP", UiTheme.FONT_BODY, xpCol);
|
||||
xpTxt.setLocalTranslation(x + w - 130, y, -17);
|
||||
detailNode.attachChild(xpTxt);
|
||||
}
|
||||
y -= 20;
|
||||
|
||||
addQuad(detailNode, x, y, w, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||
y -= 16;
|
||||
|
||||
String desc = TextRegistry.resolve(q.getDescription(), "");
|
||||
if (desc != null && !desc.isBlank()) {
|
||||
BitmapText dialogLbl = crispText("Dialog:", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
dialogLbl.setLocalTranslation(x, y, -17);
|
||||
detailNode.attachChild(dialogLbl);
|
||||
y -= 16;
|
||||
|
||||
List<String> lines = wrapText(desc, WRAP_CHARS);
|
||||
float blockH = lines.size() * 15f + 10f;
|
||||
addQuad(detailNode, x, y - blockH, w, blockH, UiTheme.COL_DIALOG_BG, -18f);
|
||||
|
||||
BitmapText q1 = crispText("\"", UiTheme.FONT_TITLE, UiTheme.COL_MUTED);
|
||||
q1.setLocalTranslation(x + 6, y - 2, -17);
|
||||
detailNode.attachChild(q1);
|
||||
for (String line : lines) {
|
||||
BitmapText lineTxt = crispText(line, UiTheme.FONT_BODY, UiTheme.COL_WHITE);
|
||||
lineTxt.setLocalTranslation(x + 18, y - 4, -17);
|
||||
detailNode.attachChild(lineTxt);
|
||||
y -= 15;
|
||||
}
|
||||
BitmapText q2 = crispText("\"", UiTheme.FONT_TITLE, UiTheme.COL_MUTED);
|
||||
q2.setLocalTranslation(x + w - 14, y - 2, -17);
|
||||
detailNode.attachChild(q2);
|
||||
y -= 18;
|
||||
}
|
||||
|
||||
addQuad(detailNode, x, y, w, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||
y -= 16;
|
||||
|
||||
BitmapText aufgabeLbl = crispText("Aufgabe:", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
aufgabeLbl.setLocalTranslation(x, y, -17);
|
||||
detailNode.attachChild(aufgabeLbl);
|
||||
y -= 18;
|
||||
|
||||
String objective = resolveObjective(q);
|
||||
for (String line : wrapText(objective, WRAP_CHARS)) {
|
||||
BitmapText lineTxt = crispText(line, UiTheme.FONT_LABEL, UiTheme.COL_WHITE);
|
||||
lineTxt.setLocalTranslation(x + 8, y, -17);
|
||||
detailNode.attachChild(lineTxt);
|
||||
y -= 17;
|
||||
}
|
||||
|
||||
if (showCompleted) {
|
||||
String successText = TextRegistry.resolve(q.getSuccessText(), "");
|
||||
if (successText != null && !successText.isBlank()) {
|
||||
y -= 8;
|
||||
addQuad(detailNode, x, y, w, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||
y -= 16;
|
||||
BitmapText abschlussLbl = crispText("Abschluss:", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
abschlussLbl.setLocalTranslation(x, y, -17);
|
||||
detailNode.attachChild(abschlussLbl);
|
||||
y -= 16;
|
||||
for (String line : wrapText(successText, WRAP_CHARS)) {
|
||||
BitmapText lineTxt = crispText(line, UiTheme.FONT_BODY, UiTheme.COL_COMPLETED);
|
||||
lineTxt.setLocalTranslation(x + 8, y, -17);
|
||||
detailNode.attachChild(lineTxt);
|
||||
y -= 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildDetail() {
|
||||
detailNode.detachAllChildren();
|
||||
buildDetail();
|
||||
}
|
||||
|
||||
private void rebuild() {
|
||||
setEnabled(false);
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private List<Quest> activeList() {
|
||||
if (mc == null) { return List.of(); }
|
||||
List<Quest> src = showCompleted ? mc.getCompletedQuests() : mc.getOpenQuests();
|
||||
return src != null ? src : List.of();
|
||||
}
|
||||
|
||||
private int visibleSlots() {
|
||||
return (int) (CONTENT_H / (ITEM_H + ITEM_GAP));
|
||||
}
|
||||
|
||||
private static String resolveTitle(Quest q) {
|
||||
String t = TextRegistry.resolve(q.getText(), q.getQuestId() != null ? q.getQuestId() : "");
|
||||
return (t == null || t.isBlank()) ? (q.getQuestId() != null ? q.getQuestId() : "Unbekannt") : t;
|
||||
}
|
||||
|
||||
private static String resolveObjective(Quest q) {
|
||||
if (q instanceof TalkQuest tq && tq.getTalkTo() != null) {
|
||||
return "Sprich mit " + tq.getTalkTo().getDisplayText();
|
||||
}
|
||||
if (q instanceof ItemQuest iq && iq.getItem() != null) {
|
||||
return "Sammle " + iq.getCount() + "× " + iq.getItem().getDisplayText();
|
||||
}
|
||||
if (q instanceof BringQuest bq) {
|
||||
String npc = bq.getBring() != null ? bq.getBring().getDisplayText() : "?";
|
||||
String loc = bq.getBringTo() != null
|
||||
? TextRegistry.resolve(bq.getBringTo().getName(), "?") : "?";
|
||||
return "Bringe " + npc + " nach " + loc;
|
||||
}
|
||||
if (q instanceof FollowQuest fq) {
|
||||
String npc = fq.getFollow() != null ? fq.getFollow().getDisplayText() : "?";
|
||||
String loc = fq.getFollowTo() != null
|
||||
? TextRegistry.resolve(fq.getFollowTo().getName(), "?") : "?";
|
||||
return "Begleite " + npc + " nach " + loc;
|
||||
}
|
||||
if (q instanceof InteractQuest iq && iq.getInteractWith() != null) {
|
||||
return "Interagiere mit " + iq.getInteractWith().getDisplayText();
|
||||
}
|
||||
return TextRegistry.resolve(q.getDescription(), "—");
|
||||
}
|
||||
|
||||
private static String typeLabel(Quest q) {
|
||||
if (q instanceof TalkQuest) { return "Gespräch"; }
|
||||
if (q instanceof ItemQuest) { return "Sammeln"; }
|
||||
if (q instanceof BringQuest) { return "Bringen"; }
|
||||
if (q instanceof FollowQuest) { return "Begleiten"; }
|
||||
if (q instanceof InteractQuest) { return "Interaktion"; }
|
||||
return "Quest";
|
||||
}
|
||||
|
||||
private static ColorRGBA typeColor(Quest q) {
|
||||
if (q instanceof TalkQuest) { return UiTheme.COL_TYPE_TALK; }
|
||||
if (q instanceof ItemQuest) { return UiTheme.COL_TYPE_ITEM; }
|
||||
if (q instanceof BringQuest) { return UiTheme.COL_TYPE_BRING; }
|
||||
if (q instanceof FollowQuest) { return UiTheme.COL_TYPE_FOLLOW; }
|
||||
if (q instanceof InteractQuest) { return UiTheme.COL_TYPE_INTERACT; }
|
||||
return UiTheme.COL_MUTED;
|
||||
}
|
||||
|
||||
private static List<String> wrapText(String text, int charsPerLine) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (text == null || text.isBlank()) { return result; }
|
||||
String[] words = text.split("\\s+");
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (String word : words) {
|
||||
if (line.length() > 0 && line.length() + 1 + word.length() > charsPerLine) {
|
||||
result.add(line.toString());
|
||||
line = new StringBuilder();
|
||||
}
|
||||
if (line.length() > 0) { line.append(' '); }
|
||||
line.append(word);
|
||||
}
|
||||
if (line.length() > 0) { result.add(line.toString()); }
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -275,6 +275,7 @@ public class RiverState extends BaseAppState {
|
||||
|
||||
// ── Worley-Noise Schaum-Textur ────────────────────────────────────────────
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private Texture2D generateFoamTexture() {
|
||||
int size = 256;
|
||||
int nPts = 40;
|
||||
@@ -322,6 +323,7 @@ public class RiverState extends BaseAppState {
|
||||
* Die Spray-Richtung ist das Spiegelbild der horizontalen Fließrichtung
|
||||
* (Wasser prallt auf und spritzt zurück + hoch).
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private void buildWaterfallParticles(RiverPoint base, Vector3f flowDir) {
|
||||
ParticleEmitter emitter = new ParticleEmitter(
|
||||
"waterfall_particles", ParticleMesh.Type.Triangle, 60);
|
||||
|
||||
@@ -284,7 +284,7 @@ public class SculptedMeshState extends BaseAppState {
|
||||
buf.put((byte) rgb[0]).put((byte) rgb[1]).put((byte) rgb[2]).put((byte) 255);
|
||||
buf.flip();
|
||||
Texture2D tex = new Texture2D(
|
||||
new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf));
|
||||
new com.jme3.texture.Image(com.jme3.texture.Image.Format.RGBA8, 1, 1, buf, ColorSpace.Linear));
|
||||
tex.setWrap(Texture.WrapMode.Repeat);
|
||||
return tex;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user