From 6cf8f52faf2127938db694dd7725561fdf4af982 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 12 Jul 2026 22:27:22 +0200 Subject: [PATCH] =?UTF-8?q?Weiter=20am=20Men=C3=BC=20und=20an=20der=20Voll?= =?UTF-8?q?bildproblematik=20gearbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .../src/main/resources/Shaders/Grass.vert | 7 +- .../main/resources/Shaders/GrassVertex.vert | 7 +- .../resources/Textures/menu/container.png | Bin 0 -> 4539 bytes .../Textures/menu/scrollbar_thumb.png | Bin 0 -> 159 bytes .../Textures/menu/scrollbar_track.png | Bin 0 -> 162 bytes .../main/resources/character/hero.character | 12 +- .../main/java/de/blight/editor/EditorApp.java | 10 +- .../java/de/blight/editor/JmeEditorApp.java | 1 + .../editor/LegacyAssetRedirectLocator.java | 2 + .../blight/editor/state/AnimPreviewState.java | 3 + .../de/blight/editor/state/EzTreeState.java | 1 + .../blight/editor/state/GrassVertexState.java | 2 +- .../editor/state/PalmGeneratorState.java | 1 + .../editor/state/PlacedObjectState.java | 2 +- .../blight/editor/state/SceneObjectState.java | 1 + .../editor/state/SculptedMeshEditorState.java | 2 +- .../blight/editor/state/StoneEditorState.java | 2 +- .../editor/state/TerrainEditorState.java | 12 +- .../editor/state/ThumbnailRenderer.java | 1 + .../editor/state/TreeGeneratorState.java | 3 +- .../blight/editor/state/VoxelEditorState.java | 3 +- .../editor/ui/LocalizationEditorView.java | 4 +- .../main/java/de/blight/game/BlightGame.java | 148 +++++- .../game/animation/AnimationLibrary.java | 2 + .../game/animation/RetargetingSystem.java | 3 + .../de/blight/game/config/ConfigScreen.java | 242 +++------ .../de/blight/game/config/GraphicsScreen.java | 216 ++------ .../de/blight/game/config/KeyBindings.java | 8 + .../java/de/blight/game/config/PauseMenu.java | 185 +------ .../game/control/ThirdPersonCamera.java | 33 +- .../java/de/blight/game/scene/WorldScene.java | 23 +- .../de/blight/game/state/CharacterState.java | 471 ++++++++++++++++++ .../de/blight/game/state/CompassHudState.java | 157 ++++++ .../java/de/blight/game/state/GrassState.java | 2 +- .../game/state/GrassVertexRenderState.java | 7 +- .../de/blight/game/state/HotbarState.java | 261 ++++++++++ .../de/blight/game/state/InventoryState.java | 293 ++++------- .../java/de/blight/game/state/QuestState.java | 462 +++++++++++++++++ .../java/de/blight/game/state/RiverState.java | 2 + .../blight/game/state/SculptedMeshState.java | 2 +- 41 files changed, 1848 insertions(+), 748 deletions(-) create mode 100644 blight-assets/src/main/resources/Textures/menu/container.png create mode 100644 blight-assets/src/main/resources/Textures/menu/scrollbar_thumb.png create mode 100644 blight-assets/src/main/resources/Textures/menu/scrollbar_track.png create mode 100644 blight-game/src/main/java/de/blight/game/state/CharacterState.java create mode 100644 blight-game/src/main/java/de/blight/game/state/CompassHudState.java create mode 100644 blight-game/src/main/java/de/blight/game/state/HotbarState.java create mode 100644 blight-game/src/main/java/de/blight/game/state/QuestState.java diff --git a/.gitignore b/.gitignore index 4d4ffe5..335bab0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ run/ # Spielstände saves/ + +# Claude Code interne Worktrees +.claude/worktrees/ diff --git a/blight-assets/src/main/resources/Shaders/Grass.vert b/blight-assets/src/main/resources/Shaders/Grass.vert index 3e80ce9..5179ebb 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.vert +++ b/blight-assets/src/main/resources/Shaders/Grass.vert @@ -30,10 +30,15 @@ void main() { float sway = sin(t * 2.1 + worldXZ.x * 0.08 + worldXZ.y * 0.06) * 0.6 + sin(t * 1.4 - worldXZ.x * 0.05 + worldXZ.y * 0.09) * 0.4; + // Mindest-Stärke damit bei wenig Wind noch Bewegung sichtbar ist + float effectiveStrength = max(m_WindStrength, 0.05); + // Quadratische Gewichtung: Spitze bewegt sich mehr als Basis - float bend = sway * m_WindStrength * inTexCoord.y * inTexCoord.y; + float bend = sway * effectiveStrength * inTexCoord.y * inTexCoord.y; pos.x += bend; pos.z += bend * 0.3; + // Y-Kompression: Halm neigt sich statt zu strecken → verhindert visuelles Breiterwerden + pos.y -= bend * bend * 0.5; } texCoord = inTexCoord; diff --git a/blight-assets/src/main/resources/Shaders/GrassVertex.vert b/blight-assets/src/main/resources/Shaders/GrassVertex.vert index da05eb9..73a8227 100644 --- a/blight-assets/src/main/resources/Shaders/GrassVertex.vert +++ b/blight-assets/src/main/resources/Shaders/GrassVertex.vert @@ -34,10 +34,15 @@ void main() { float sway = sin(t * 2.1 + wavePhase * 0.10 + randPhase) * 0.6 + sin(t * 1.4 + wavePhase * 0.06 + randPhase * 0.73) * 0.4; + // Mindest-Stärke damit bei wenig Wind noch Bewegung sichtbar ist + float effectiveStrength = max(m_WindStrength, 0.05); + // Quadratische Gewichtung: Spitze biegt sich mehr als Basis - float bend = sway * m_WindStrength * wf * wf; + float bend = sway * effectiveStrength * wf * wf; pos.x += windN.x * bend; pos.z += windN.y * bend; + // Y-Kompression: Halm neigt sich statt zu strecken → verhindert visuelles Breiterwerden + pos.y -= bend * bend * 0.5; } varColor = inColor; diff --git a/blight-assets/src/main/resources/Textures/menu/container.png b/blight-assets/src/main/resources/Textures/menu/container.png new file mode 100644 index 0000000000000000000000000000000000000000..a656cda6b8c2ca2f69d10ea10917bcd4b6998608 GIT binary patch literal 4539 zcmeHKdsGuw8V|N53L+M)kFMe{x?;u2%p`#%W5P2$qDBy+Jk++6nL9WO$%M>6BInpv zgw|KF_$aN?ZXdo-t5#d=LARo=vRkpzRgbl<$f~XD3(?j3mc0`m;%U$6dd~Jg=A6mQ zeBbZ;-S7L|@7|NUFnvl&P{4};3WXxbl4{O?&p7$+>kof7@`cUtvA@KYEoIOi#N`$^ zz5pQ792Y=8}kpE~@NF?t8OZOCiU^Qm=&< zY-&2|e+(aWGmiPBGIrmF^5;JeH1w~FjkFvsnY*uc{R@|r{-1AQ+K(Nty*;)#IJ|Ym zOT$7J2FIQ2o4#|+i@_)TYqy4GzCUw+z{~qqe?ZCTtoors`4Ym>|od#h#;4}9Qvb|f-okt*Zt?PUpzhXz;NZ)j|!yjL7J@Vc9pD_Pt&1ZAC_oA=+U%$D| z;cAYk*uHFY$e%-(u9o7@rfi7}+f%5j8{gKr;eE@I(3FJ-?8?XyjT6@;8W8 z%1Pl_!kK;%DuhqEuNjzY4^TKi$=fEI;7$5(B~3%#qDU-pJ}K#YV<2TQopkK z=d3@}H%45%RXcb>AOG5Ui!ax%yW&?hp}uK(`0DME&+!4}ecE12KC|K(e_>wRsuMHv zN3IW!d$VrhX@&Bw`s~W>C+0SvK6>)hp1j=Q>45m)W=J~ciD`ihz}y=o6u})I+7^30isoDRRosg<%`v5 zOaNkZvmBLSPVS_DUnX?6B)KRY_jo)ik47c9^KjB&FyMq5SF16Iz{ELDiS}Ynag78Py3Q!N+OS^DVMc@tx-ZMg!l8PZo zXF|UkA==j#*S%en#K%7G{Ar+3K%#YY1c%tJ*X^BQKFp;kf|WKiibRmjsQBw z9*qH-(PCOcLtqA)&|!8?ZD2_fs98eagCfn%!>*(YdS)e4v5<;pbbw$ZJ7789yg5sxkD1+>J$ z024ZycNTdcJ8ZlIWJKSm;i53DMytW}Q3fprI8Bs>j@AN}?hvV) zT@*OUL%TtI9@G(P1>4i76*AU0Q7`#4>qdKK19=vpU>Iz$UkZkI7L3b#Mpwl~{1=)S zeSjXD42b=i>Jq0zrBQc`AK-Dixy@+Q8#mr$b9WS89%^Py~j` zx1VBF^-yS3N)~I9@{%&3ABLm5Z}fx5Y1U$nw|RfKdWv-JBL*G3eqn5c`zO=i`_;@)CXabXU39TJ&=y8bUofx#or%)y`PJGr>n4TXZyyuqfA{{~zth`RdTmo1 zgAeC_cB?se!t%;@C&V%vuk5^Dlh*d*#fP_4UN&`;~<~3Q0gKvqFwux?L21bHqCbYwvv)kx0gFQ~q_pN7FZ5 z9dpT4pF3d}7ySKcqY&Ht@(?CSYHz#a_RNgEy9Ar|@(v{|^MAj}W>$wjIAn-FBYNAX zo@iVumZHmsW`ecfT>ieaYTVYhYt&TS`O8$!R6Vis9^;=5dG)1h=I;X9$l&Sf=d#Wzp$Pyy C2Q+p7 literal 0 HcmV?d00001 diff --git a/blight-assets/src/main/resources/Textures/menu/scrollbar_track.png b/blight-assets/src/main/resources/Textures/menu/scrollbar_track.png new file mode 100644 index 0000000000000000000000000000000000000000..f60c387cfc070073b31287c7b4d7685448361e7d GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3QkkAEjv*Cul2aU*q@>K$B{~?) z^JF$QY?Pd6y07Z1LyH9Sks~7a4)7#3aSE-LjMZJ-)^?UN$Yk;Emd*`Ee-f@V1Twqv zN|=Hf0X#bjA9t{>S*Og}(W$w|aiz4UAxosL@+KC`tJVzln^Yt3O`j?Rw3NZq)z4*} HQ$iB}!$~vO literal 0 HcmV?d00001 diff --git a/blight-assets/src/main/resources/character/hero.character b/blight-assets/src/main/resources/character/hero.character index c4600d0..12c68ab 100644 --- a/blight-assets/src/main/resources/character/hero.character +++ b/blight-assets/src/main/resources/character/hero.character @@ -2,12 +2,12 @@ "chapter": 0, "level": 0, "xp": 0, - "currentHp": 0, - "currentStamina": 0, - "currentMana": 0, - "maxHp": 0, - "maxStamina": 0, - "maxMana": 0, + "currentHp": 100, + "currentStamina": 100, + "currentMana": 100, + "maxHp": 100, + "maxStamina": 100, + "maxMana": 100, "openHpRegeneration": 0, "openManaRegeneration": 0, "openStaminaRegeneration": 0, diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index da43e4c..f5cf9df 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -7330,6 +7330,11 @@ public class EditorApp extends Application { private void startGameProcess() { final boolean isNewGame = pendingNewGame; pendingNewGame = false; + // Editor minimieren, damit das GLFW-Vollbild-Fenster keinen Focus-Konkurrenten hat. + // GLFW_AUTO_ICONIFY würde das Vollbild-Fenster sonst sofort minimieren, weil der + // Editor beim Starten noch den Fokus hat. Der JVM-Start des Spiels dauert mehrere + // Sekunden – das Fenster ist längst minimiert, bevor GLFW das Fenster anlegt. + Platform.runLater(() -> { if (primaryStage != null) primaryStage.setIconified(true); }); new Thread(() -> { try { String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString(); @@ -7387,9 +7392,10 @@ public class EditorApp extends Application { consoleBuffer.offer(line); } } - // Spiel beendet → Buttons freigeben + // Spiel beendet → Buttons freigeben und Editor wiederherstellen consoleBuffer.offer("--- Spiel beendet ---"); Platform.runLater(() -> { + if (primaryStage != null) primaryStage.setIconified(false); if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); } }); @@ -8863,7 +8869,7 @@ public class EditorApp extends Application { Spinner spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0); Spinner spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0); Spinner spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0); - for (Spinner sp : new Spinner[]{spTX, spTY, spTZ, spRX, spRY, spRZ}) { + for (Spinner sp : java.util.List.of(spTX, spTY, spTZ, spRX, spRY, spRZ)) { sp.setEditable(true); sp.setMaxWidth(Double.MAX_VALUE); } diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index 08fb9b2..b9c2aa4 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -262,6 +262,7 @@ public class JmeEditorApp extends SimpleApplication { // ── Framebuffer-Verwaltung ──────────────────────────────────────────────── + @SuppressWarnings("deprecation") private void buildFrameBuffer(int w, int h, WritableImage image) { Texture2D colorTex = new Texture2D(w, h, Image.Format.RGBA8); FrameBuffer fb = new FrameBuffer(w, h, 1); diff --git a/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java b/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java index 9315b2f..57c48a8 100644 --- a/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java +++ b/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java @@ -37,6 +37,7 @@ public class LegacyAssetRedirectLocator implements AssetLocator { } @Override + @SuppressWarnings("rawtypes") public AssetInfo locate(AssetManager manager, AssetKey key) { String name = key.getName(); for (var entry : PREFIXES.entrySet()) { @@ -55,6 +56,7 @@ public class LegacyAssetRedirectLocator implements AssetLocator { return null; } + @SuppressWarnings("rawtypes") private AssetInfo tryFile(AssetManager manager, AssetKey key, String rel) { Path p = root.resolve(rel); if (!Files.exists(p)) return null; diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index 8c86b0b..fbd2999 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -454,6 +454,7 @@ public class AnimPreviewState extends BaseAppState { input.skeletalPaths.set(Collections.unmodifiableSet(result)); } + @SuppressWarnings("deprecation") private boolean hasSkeleton(Spatial s) { if (s.getControl(AnimComposer.class) != null) return true; // Fallback auf altes AnimControl (Legacy-Modelle) @@ -775,6 +776,7 @@ public class AnimPreviewState extends BaseAppState { // ── Framebuffer ─────────────────────────────────────────────────────────── + @SuppressWarnings("deprecation") private FrameBuffer buildFrameBuffer(int w, int h) { FrameBuffer fb = new FrameBuffer(w, h, 1); fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8)); @@ -1023,6 +1025,7 @@ public class AnimPreviewState extends BaseAppState { * Hält jeden Root-Joint an seiner lokalen Bind-Pose-Transform → SC-Matrix = I → T-Pose. * Gibt null zurück wenn das Armature keine Root-Joints hat. */ + @SuppressWarnings("rawtypes") private static AnimClip buildTPoseClip(com.jme3.anim.Armature armature) { int jointCount = armature.getJointCount(); if (jointCount == 0) return null; diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java index 387a953..d0e1303 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -383,6 +383,7 @@ public class EzTreeState extends BaseAppState { // ── Phase 2: Impostor-Capture (4-Richtungen) ───────────────────────────── + @SuppressWarnings("deprecation") private void startCapturePass(int pass, Node treeNode, BoundingBox bb) { BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f); Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); diff --git a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java index b31e43e..239e6de 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java @@ -86,7 +86,7 @@ public class GrassVertexState extends BaseAppState { private Material[] seedMaterials = new Material[0]; private Material seedStalkMaterial; - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private final List[] chunkBlades = new List[CHUNK_COUNT]; private final Node[] chunkNodes = new Node[CHUNK_COUNT]; private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT]; diff --git a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java index a3f97a1..40f38b1 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java @@ -155,6 +155,7 @@ public class PalmGeneratorState extends BaseAppState { // ── Phase 2: Capture ───────────────────────────────────────────────────── + @SuppressWarnings("deprecation") private void startCapturePass(int pass) { Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java index 627b3f0..74f755f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java @@ -72,7 +72,7 @@ public class PlacedObjectState extends BaseAppState { private Node grassNode; private final Map slotMaterials = new LinkedHashMap<>(); - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private final List[] chunkTufts = new List[CHUNK_COUNT]; private final Node[] chunkNodes = new Node[CHUNK_COUNT]; private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT]; diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index 4ff41e5..3fe1dfe 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -1368,6 +1368,7 @@ public class SceneObjectState extends BaseAppState { // ── Modell-Konvertierung ────────────────────────────────────────────────── + @SuppressWarnings("deprecation") private void convertModel(SharedInput.ModelConvertRequest req) { setStatus("Konvertiere " + req.assetPath() + " …"); try { diff --git a/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java index a7a70c3..056450e 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SculptedMeshEditorState.java @@ -495,7 +495,7 @@ public class SculptedMeshEditorState extends BaseAppState { } private static int[][] buildNeighbors(int wCount, int[] indices) { - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) Set[] sets = new Set[wCount]; for (int i = 0; i < wCount; i++) sets[i] = new HashSet<>(); for (int i = 0; i < indices.length; i += 3) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java index be1b068..f9eaadf 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java @@ -57,7 +57,7 @@ public class StoneEditorState extends BaseAppState { private Node rootNode; private Node stoneRoot; - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private final List[] chunkStones = new List[CHUNK_COUNT]; /** Pro Chunk: Node mit LOD0-Geometrien. */ private final Node[] lod0Nodes = new Node[CHUNK_COUNT]; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 6a8c429..bbe97e4 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -23,6 +23,7 @@ import com.jme3.terrain.geomipmap.TerrainQuad; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; import com.jme3.util.BufferUtils; import com.jme3.util.SkyFactory; import de.blight.common.EmitterIO; @@ -492,7 +493,7 @@ public class TerrainEditorState extends BaseAppState { splatBuf.put(splatR[i]).put(splatG[i]).put(splatB[i]).put(splatA[i]); } splatBuf.flip(); - splatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, splatBuf); + splatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, splatBuf, ColorSpace.Linear); splatTex = new Texture2D(splatImage); splatTex.setWrap(Texture.WrapMode.EdgeClamp); splatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); @@ -503,7 +504,7 @@ public class TerrainEditorState extends BaseAppState { upperSplatBuf.put(upperSplatR[i]).put(upperSplatG[i]).put(upperSplatB[i]).put(upperSplatA[i]); } upperSplatBuf.flip(); - upperSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, upperSplatBuf); + upperSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, upperSplatBuf, ColorSpace.Linear); upperSplatTex = new Texture2D(upperSplatImage); upperSplatTex.setWrap(Texture.WrapMode.EdgeClamp); upperSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); @@ -514,7 +515,7 @@ public class TerrainEditorState extends BaseAppState { thirdSplatBuf.put(thirdSplatR[i]).put(thirdSplatG[i]).put(thirdSplatB[i]).put(thirdSplatA[i]); } thirdSplatBuf.flip(); - thirdSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, thirdSplatBuf); + thirdSplatImage = new Image(Image.Format.RGBA8, SPLAT_SIZE, SPLAT_SIZE, thirdSplatBuf, ColorSpace.Linear); thirdSplatTex = new Texture2D(thirdSplatImage); thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp); thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); @@ -617,13 +618,13 @@ public class TerrainEditorState extends BaseAppState { if (path != null && !path.isEmpty()) { try { ByteBuffer buf = loadTextureToRGBA8(path, size); - 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("[TerrainArray] Slot {} nicht ladbar ({}): {}", i, path, e.getMessage()); } } - images.add(new Image(Image.Format.RGBA8, size, size, solidColorLayer(fb, size))); + images.add(new Image(Image.Format.RGBA8, size, size, solidColorLayer(fb, size), ColorSpace.Linear)); } com.jme3.texture.TextureArray texArr = new com.jme3.texture.TextureArray(images); @@ -1769,6 +1770,7 @@ public class TerrainEditorState extends BaseAppState { return g; } + @SuppressWarnings("deprecation") private void addGizmoLabel(Node parent, Vector3f pos, String text, ColorRGBA color) { try { com.jme3.font.BitmapFont font = assets.loadFont("Interface/Fonts/Default.fnt"); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java b/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java index 68cd4a0..41c2508 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java @@ -159,6 +159,7 @@ public final class ThumbnailRenderer { // ── Intern ─────────────────────────────────────────────────────────────── + @SuppressWarnings("deprecation") private static byte[] renderToFramebuffer(Node scene, Camera cam, RenderManager rm, Renderer renderer) { Texture2D colorTex = new Texture2D(SIZE, SIZE, Image.Format.RGBA8); diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java index ab046e6..0d5a071 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java @@ -224,6 +224,7 @@ public class TreeGeneratorState extends BaseAppState { // ── Framebuffer-Helpers ─────────────────────────────────────────────────── + @SuppressWarnings("deprecation") private FrameBuffer buildFrameBuffer(int w, int h) { FrameBuffer fb = new FrameBuffer(w, h, 1); fb.addColorTexture(new Texture2D(w, h, Image.Format.RGBA8)); @@ -429,7 +430,7 @@ public class TreeGeneratorState extends BaseAppState { return atlas; } - /** Startet einen einzelnen Capture-Durchlauf für die gegebene Richtung (Pass 0..3). */ + @SuppressWarnings("deprecation") private void startCapturePass(int pass) { captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index 07fabcf..04b27fa 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -1565,7 +1565,8 @@ public class VoxelEditorState extends BaseAppState { buf.put((byte) rgb[0]).put((byte) rgb[1]).put((byte) rgb[2]).put((byte) 255); buf.flip(); com.jme3.texture.Texture2D tex = new com.jme3.texture.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, + com.jme3.texture.image.ColorSpace.Linear)); tex.setWrap(Texture.WrapMode.Repeat); return tex; } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java index 499a279..2b813c8 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java @@ -71,7 +71,9 @@ public class LocalizationEditorView extends BorderPane { table.setEditable(true); table.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"); - table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + @SuppressWarnings("deprecation") + var policy = TableView.CONSTRAINED_RESIZE_POLICY; + table.setColumnResizePolicy(policy); TableColumn keyCol = new TableColumn<>("Schlüssel"); keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0])); diff --git a/blight-game/src/main/java/de/blight/game/BlightGame.java b/blight-game/src/main/java/de/blight/game/BlightGame.java index 4c1621c..497f160 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -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); + } + } } diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java index f171f10..e8b3de3 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java @@ -251,6 +251,7 @@ public class AnimationLibrary extends BaseAppState { } } + @SuppressWarnings("rawtypes") private static AnimClip extractSubClip(AnimClip source, String name, float startSec, float endSec) { List> 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; diff --git a/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java b/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java index f7e1877..ea5f327 100644 --- a/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java +++ b/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java @@ -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> 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); diff --git a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java index 48c4298..97602fd 100644 --- a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java @@ -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 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 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); } } diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java index e79b462..d6bd0cf 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java @@ -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; - } } diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java index 1589abb..83859df 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java @@ -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) { diff --git a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java index 906e478..0c8ea93 100644 --- a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java +++ b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java @@ -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; - } } diff --git a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java index b6f2d89..f1bd379 100644 --- a/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java +++ b/blight-game/src/main/java/de/blight/game/control/ThirdPersonCamera.java @@ -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) { diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 81ede51..810adf0 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -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)); } } diff --git a/blight-game/src/main/java/de/blight/game/state/CharacterState.java b/blight-game/src/main/java/de/blight/game/state/CharacterState.java new file mode 100644 index 0000000..7761225 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/CharacterState.java @@ -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 combat = learnedCombat(ab); + List 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 combat = learnedCombat(ab); + List 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 combat, List 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 combat, List 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 learnedCombat(Abilities ab) { + List 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 learnedCraft(Abilities ab) { + List 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"}}; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/CompassHudState.java b/blight-game/src/main/java/de/blight/game/state/CompassHudState.java new file mode 100644 index 0000000..f68c2c9 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/CompassHudState.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/GrassState.java b/blight-game/src/main/java/de/blight/game/state/GrassState.java index 429b002..019abd4 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassState.java @@ -47,7 +47,7 @@ public class GrassState extends BaseAppState private Node grassNode; - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "rawtypes"}) private final List[] chunkTufts = new List[CHUNK_COUNT]; private final Node[] chunkNodes = new Node[CHUNK_COUNT]; private final Map slotMaterials = new LinkedHashMap<>(); diff --git a/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java b/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java index 5b4bc94..cfa1521 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java @@ -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[] 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 diff --git a/blight-game/src/main/java/de/blight/game/state/HotbarState.java b/blight-game/src/main/java/de/blight/game/state/HotbarState.java new file mode 100644 index 0000000..ec810db --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/HotbarState.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/InventoryState.java b/blight-game/src/main/java/de/blight/game/state/InventoryState.java index 39c0ba2..3b8f11d 100644 --- a/blight-game/src/main/java/de/blight/game/state/InventoryState.java +++ b/blight-game/src/main/java/de/blight/game/state/InventoryState.java @@ -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>> groups = new LinkedHashMap<>(); for (Map.Entry 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>> 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> groupItems = group.getValue(); int col = 0; for (Map.Entry 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>> groups = new LinkedHashMap<>(); for (Map.Entry 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> 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) + "…"; } diff --git a/blight-game/src/main/java/de/blight/game/state/QuestState.java b/blight-game/src/main/java/de/blight/game/state/QuestState.java new file mode 100644 index 0000000..ef34223 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/QuestState.java @@ -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 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 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 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 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 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 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 activeList() { + if (mc == null) { return List.of(); } + List 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 wrapText(String text, int charsPerLine) { + List 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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/RiverState.java b/blight-game/src/main/java/de/blight/game/state/RiverState.java index aa408b3..6e05758 100644 --- a/blight-game/src/main/java/de/blight/game/state/RiverState.java +++ b/blight-game/src/main/java/de/blight/game/state/RiverState.java @@ -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); diff --git a/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java b/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java index 54db675..459ac35 100644 --- a/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java +++ b/blight-game/src/main/java/de/blight/game/state/SculptedMeshState.java @@ -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; }