Weitere Bugfixes und kleine Feature Erweiterungen

This commit is contained in:
2026-08-10 19:41:32 +02:00
parent 753af2dc13
commit 145db8317f
20 changed files with 652 additions and 211 deletions

View File

@@ -339,12 +339,13 @@ public class BlightGame extends SimpleApplication {
super.initialize(sm, a);
setDisplayFps(false);
setDisplayStatView(false);
int h = a.getCamera().getHeight();
if (fpsText != null) fpsText.setLocalTranslation(0, h, 0);
if (statsView != null) {
float fpsH = fpsText != null ? fpsText.getLineHeight() : 0f;
statsView.setLocalTranslation(0, h - fpsH, 0);
}
int h = a.getCamera().getHeight();
float fpsH = fpsText != null ? fpsText.getLineHeight() : 0f;
float statsH = statsView != null ? statsView.getHeight() : 0f;
if (fpsText != null) fpsText.setLocalTranslation(0, h, 0);
if (statsView != null) statsView.setLocalTranslation(0, h - fpsH - statsH, 0);
if (darkenFps != null) darkenFps.setLocalTranslation(0, h - fpsH, -1);
if (darkenStats != null) darkenStats.setLocalTranslation(0, h - fpsH - statsH, -1);
}
};
stateManager.attach(statsState);

View File

@@ -0,0 +1,72 @@
package de.blight.game;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
/**
* Leitet veraltete Textur-Pfade auf die aktuellen Speicherorte um.
* Behebt Laden von j3o-Dateien, die noch den alten Textur-Pfad
* (z. B. Textures/internal/leaves/) statt des neuen (internal/foliage/) enthalten.
*
* Registriert mit root = blight-assets/src/main/resources.
*/
public class LegacyAssetRedirectLocator implements AssetLocator {
private static final Map<String, String> PREFIXES = Map.of(
"Textures/bark/", "Textures/internal/bark/",
"Textures/leaves/", "Textures/internal/foliage/",
"Textures/internal/leaves/", "Textures/internal/foliage/",
"Textures/fern/", "Textures/internal/fern/",
"Textures/water/", "Textures/internal/water/",
"Textures/Water/", "Textures/internal/water/"
);
private Path root;
@Override
public void setRootPath(String rootPath) {
root = Paths.get(rootPath);
}
@Override
@SuppressWarnings("rawtypes")
public AssetInfo locate(AssetManager manager, AssetKey key) {
String name = key.getName();
for (var entry : PREFIXES.entrySet()) {
if (name.startsWith(entry.getKey())) {
String remapped = entry.getValue() + name.substring(entry.getKey().length());
AssetInfo info = tryFile(manager, key, remapped);
if (info != null) return info;
if (remapped.toLowerCase().endsWith(".tga")) {
info = tryFile(manager, key, remapped.substring(0, remapped.length() - 4) + ".png");
if (info != null) return info;
}
break;
}
}
return null;
}
@SuppressWarnings("rawtypes")
private AssetInfo tryFile(AssetManager manager, AssetKey key, String rel) {
Path p = root.resolve(rel);
if (!Files.exists(p)) return null;
return new AssetInfo(manager, key) {
@Override
public InputStream openStream() {
try { return new FileInputStream(p.toFile()); }
catch (FileNotFoundException ex) { throw new RuntimeException(ex); }
}
};
}
}

View File

@@ -46,6 +46,10 @@ public abstract class MenuScreen extends BaseAppState {
public static final float CONT_W = MenuCanvas.CONT_W;
public static final float CONT_H = MenuCanvas.CONT_H;
private static int openCount = 0;
public static boolean isAnyOpen() { return openCount > 0; }
public enum BtnStyle { DEFAULT, SAVE, QUIT, DISABLED, ARROW }
protected SimpleApplication app;
@@ -77,6 +81,7 @@ public abstract class MenuScreen extends BaseAppState {
@Override
protected void onEnable() {
openCount++;
uiScale = MenuCanvas.getScale(app.getCamera());
bgLayer = buildBgLayer();
canvasNode = buildCanvas();
@@ -98,6 +103,7 @@ public abstract class MenuScreen extends BaseAppState {
@Override
protected void onDisable() {
if (openCount > 0) { openCount--; }
onDisableExtras();
app.getInputManager().removeListener(clickListener);
if (app.getInputManager().hasMapping(MAP_CLICK)) {

View File

@@ -40,6 +40,8 @@ public abstract class OverlayState extends BaseAppState {
private static OverlayState activeOverlay;
public static boolean isAnyOpen() { return activeOverlay != null; }
// ── Panel-Dimensionen (90% des Referenzraums) ─────────────────────────────
public static final float PW = MenuCanvas.CONT_W;

View File

@@ -232,9 +232,9 @@ public class WorldScene extends BaseAppState {
@Override
protected void onEnable() {
try {
assetManager.registerLocator(
AnimationLibrary.findAssetRoot().toAbsolutePath().toString(),
com.jme3.asset.plugins.FileLocator.class);
String assetRootStr = AnimationLibrary.findAssetRoot().toAbsolutePath().toString();
assetManager.registerLocator(assetRootStr, com.jme3.asset.plugins.FileLocator.class);
assetManager.registerLocator(assetRootStr, de.blight.game.LegacyAssetRedirectLocator.class);
} catch (Exception ignored) {}
app.getCamera().setFrustumPerspective(45f,
@@ -271,7 +271,7 @@ public class WorldScene extends BaseAppState {
// Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1)
CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1);
physicsChar = new CharacterControl(capsule, 0.05f);
physicsChar.setJumpSpeed(12f);
physicsChar.setJumpSpeed(7f);
physicsChar.setFallSpeed(35f);
physicsChar.setGravity(35f);
character.addControl(physicsChar);

View File

@@ -4,8 +4,6 @@ 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;
@@ -15,51 +13,48 @@ 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.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.MenuScreen;
import de.blight.game.config.OverlayState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Kompass unten links runde rotierende Rose, identisch mit dem Editor-Kompass.
* Textur-basierter Kompass unten links.
*
* Die Rose dreht sich mit dem Kamera-Yaw; die Buchstaben konter-rotieren,
* damit sie immer aufrecht bleiben. Ein festes goldenes Dreieck oben zeigt
* die aktuelle Blickrichtung an.
* compass_rose.png (100×100) dreht sich mit dem Kamera-Yaw.
* compass_needle.png (100×100) fix, zeigt immer nach oben (= aktuelle Blickrichtung).
*
* Beide Dateien werden beim ersten Start als Platzhalter erzeugt
* und können durch eigene Grafiken ersetzt werden.
*/
public class CompassHudState extends BaseAppState {
// ── Layout ────────────────────────────────────────────────────────────────
private static final float SIZE = 100f; // Canvas-Größe (px)
private static final float RADIUS = 47f; // Kreis-Außenradius
private static final float LABEL_R = 30f; // Beschriftungsradius vom Mittelpunkt
private static final float MARGIN = 16f; // Abstand zum Bildschirmrand
private static final Logger log = LoggerFactory.getLogger(CompassHudState.class);
// ── Farben (matching EditorApp.drawCompass) ───────────────────────────────
private static final ColorRGBA COL_BG = new ColorRGBA(0.047f, 0.047f, 0.094f, 0.82f);
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.392f, 0.392f, 0.627f, 0.85f);
private static final ColorRGBA COL_TICK_MJ = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.65f);
private static final ColorRGBA COL_TICK_MN = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.45f);
private static final ColorRGBA COL_N = new ColorRGBA(1.00f, 0.314f, 0.314f, 1.00f);
private static final ColorRGBA COL_CARD = new ColorRGBA(0.824f, 0.824f, 0.902f, 1.00f);
private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.863f, 0.235f, 0.95f);
private static final ColorRGBA COL_CENTER = new ColorRGBA(0.784f, 0.784f, 0.941f, 0.85f);
// Kompasswinkel (Grad von Nord CW): N=0, O=90, S=180, W=270
private static final String[] LABELS = {"N", "O", "S", "W"};
private static final float[] LABEL_DEG = {0f, 90f, 180f, 270f};
private static final float SIZE = 100f;
private static final float MARGIN = 16f;
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private BitmapFont font;
private Node compassNode;
private Node roseNode;
private Node compassNode;
private Node roseNode;
private final Node[] labelNodes = new Node[4];
private final Quaternion roseRot = new Quaternion();
private final Quaternion labelRot = new Quaternion();
private final Quaternion roseRot = new Quaternion();
private boolean lastMenuOpen = false;
// ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -68,7 +63,7 @@ public class CompassHudState extends BaseAppState {
app = (SimpleApplication) application;
cam = app.getCamera();
assets = app.getAssetManager();
font = assets.loadFont("Interface/Fonts/Default.fnt");
ensureAssets();
}
@Override
@@ -94,169 +89,160 @@ public class CompassHudState extends BaseAppState {
public void update(float tpf) {
if (compassNode == null) { return; }
Vector3f dir = cam.getDirection();
// yaw=0 = Blick nach -Z (Nord); JME3-Kamera blickt in -Z bei Norden
float yaw = FastMath.atan2(dir.x, -dir.z);
boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen();
if (menuOpen != lastMenuOpen) {
lastMenuOpen = menuOpen;
compassNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit);
}
if (menuOpen) { return; }
// Rose dreht sich so, dass Nord immer in der echten Nord-Richtung liegt
Vector3f dir = cam.getDirection();
float yaw = FastMath.atan2(dir.x, -dir.z);
roseRot.fromAngleAxis(-yaw, Vector3f.UNIT_Z);
roseNode.setLocalRotation(roseRot);
// Buchstaben konter-rotieren → bleiben immer aufrecht
labelRot.fromAngleAxis(yaw, Vector3f.UNIT_Z);
for (Node ln : labelNodes) {
ln.setLocalRotation(labelRot);
}
}
// ── Aufbau ────────────────────────────────────────────────────────────────
private void buildCompass() {
float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f;
float cx = MARGIN + SIZE / 2f;
float cy = oy + SIZE / 2f;
// Textur-Größen auslesen → echte Pixelgröße bestimmt Layout und Quad-Größe
float roseW = SIZE, roseH = SIZE;
try {
com.jme3.texture.Texture roseTex = assets.loadTexture("Textures/hud/compass_rose.png");
roseW = roseTex.getImage().getWidth();
roseH = roseTex.getImage().getHeight();
} catch (Exception ignored) {}
// compassNode-Mittelpunkt so setzen, dass die Rose mit MARGIN Abstand zur Ecke endet
float cx = MARGIN + roseW / 2f;
float cy = MARGIN + roseH / 2f;
compassNode = new Node("compass");
compassNode.setLocalTranslation(cx, cy, 0f);
// Rahmen (etwas größerer Kreis) + Hintergrund
compassNode.attachChild(makeCircle(RADIUS + 1.5f, 64, COL_BORDER, 1f));
compassNode.attachChild(makeCircle(RADIUS, 64, COL_BG, 2f));
// Rotierende Rose
roseNode = new Node("rose");
compassNode.attachChild(roseNode);
roseNode.attachChild(makeTexQuad("Textures/hud/compass_rose.png", 2f));
// 8 Tick-Striche (major = Hauptrichtungen, minor = Zwischenrichtungen)
for (int i = 0; i < 8; i++) {
float deg = i * 45f;
boolean maj = (i % 2 == 0);
float innerR = RADIUS - (maj ? 9f : 5f);
roseNode.attachChild(makeTickQuad(deg, innerR, maj ? COL_TICK_MJ : COL_TICK_MN, 3f));
}
// Himmelsrichtungs-Beschriftungen
for (int i = 0; i < 4; i++) {
float ca = LABEL_DEG[i] * FastMath.DEG_TO_RAD;
float lx = FastMath.sin(ca) * LABEL_R;
float ly = FastMath.cos(ca) * LABEL_R;
BitmapText lbl = makeBitmapText(LABELS[i], i == 0 ? 14 : 12, i == 0 ? COL_N : COL_CARD);
// Buchstabe am Label-Ursprung zentrieren
lbl.setLocalTranslation(-lbl.getLineWidth() * 0.5f, lbl.getLineHeight() * 0.4f, 0f);
Node ln = new Node("lbl_" + LABELS[i]);
ln.setLocalTranslation(lx, ly, 4f);
ln.attachChild(lbl);
labelNodes[i] = ln;
roseNode.attachChild(ln);
}
// Fixer Richtungs-Zeiger: gelbes Dreieck oben (nicht in roseNode)
compassNode.attachChild(makeTriangle(COL_MARKER, 5f));
// Mittelpunkt-Punkt
compassNode.attachChild(makeCircle(2.5f, 16, COL_CENTER, 6f));
// Nadel zentriert auf dem Kompass-Mittelpunkt, auf natürlicher Textur-Größe
compassNode.attachChild(makeTexQuad("Textures/hud/compass_needle.png", 5f));
app.getGuiNode().attachChild(compassNode);
}
// ── Geometry-Helfer ───────────────────────────────────────────────────────
/** Gefüllter Kreis (Triangle-Fan) zentriert bei (0,0). */
private Geometry makeCircle(float r, int segs, ColorRGBA col, float z) {
float[] verts = new float[(segs + 2) * 3];
// Mittelpunkt
verts[0] = 0f; verts[1] = 0f; verts[2] = 0f;
for (int i = 0; i <= segs; i++) {
float a = FastMath.TWO_PI * i / segs;
int base = (i + 1) * 3;
verts[base] = FastMath.cos(a) * r;
verts[base + 1] = FastMath.sin(a) * r;
verts[base + 2] = 0f;
/** Lädt Textur, erzeugt Quad in natürlicher Textur-Größe, zentriert bei (0,0). */
private Geometry makeTexQuad(String assetPath, float z) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
float w = SIZE, h = SIZE;
try {
Texture tex = assets.loadTexture(assetPath);
w = tex.getImage().getWidth();
h = tex.getImage().getHeight();
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.5f));
}
int[] idx = new int[segs * 3];
for (int i = 0; i < segs; i++) {
idx[i * 3] = 0;
idx[i * 3 + 1] = i + 1;
idx[i * 3 + 2] = i + 2;
}
Geometry g = new Geometry("circle", buildMesh(verts, idx));
g.setMaterial(makeMat(col));
g.setLocalTranslation(0f, 0f, z);
g.setQueueBucket(col.a < 1f ? RenderQueue.Bucket.Transparent : RenderQueue.Bucket.Gui);
String name = "cmp_" + assetPath.substring(assetPath.lastIndexOf('/') + 1);
Geometry g = new Geometry(name, new Quad(w, h));
g.setLocalTranslation(-w / 2f, -h / 2f, z);
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Gui);
return g;
}
/**
* Tick-Strich als dünnes Quad entlang der radialen Richtung.
* ca_deg: Kompasswinkel (0=Nord, 90=Ost, CW).
*/
private Geometry makeTickQuad(float ca_deg, float innerR, ColorRGBA col, float z) {
float ca = ca_deg * FastMath.DEG_TO_RAD;
float sx = FastMath.sin(ca); // Richtung entlang des Radius
float cy_ = FastMath.cos(ca);
float px = -cy_ * 0.75f; // Halbe Breite (senkrecht zur Radiale)
float py = sx * 0.75f;
// ── Placeholder-PNGs ──────────────────────────────────────────────────────
float[] v = {
sx * innerR + px, cy_ * innerR + py, z, // v0 links innen
sx * innerR - px, cy_ * innerR - py, z, // v1 rechts innen
sx * RADIUS + px, cy_ * RADIUS + py, z, // v2 links außen
sx * RADIUS - px, cy_ * RADIUS - py, z // v3 rechts außen
};
int[] idx = {0, 1, 2, 1, 3, 2};
Geometry g = new Geometry("tick", buildMesh(v, idx));
g.setMaterial(makeMat(col));
g.setQueueBucket(RenderQueue.Bucket.Transparent);
return g;
}
/**
* Festes gelbes Dreieck zeigt immer nach oben (Blickrichtung).
* Spitze bei (0, RADIUS5), Basis bei (±5, RADIUS16).
*/
private Geometry makeTriangle(ColorRGBA col, float z) {
float tipY = RADIUS - 5f;
float baseY = RADIUS - 16f;
float[] v = {
0f, tipY, 0f, // Spitze
-5f, baseY, 0f, // Basis links
5f, baseY, 0f // Basis rechts
};
// CCW von +Z-Seite: (0,high)→(-5,low)→(5,low) ✓
int[] idx = {0, 1, 2};
Geometry g = new Geometry("triangle", buildMesh(v, idx));
g.setMaterial(makeMat(col));
g.setLocalTranslation(0f, 0f, z);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
return g;
}
private static Mesh buildMesh(float[] verts, int[] idx) {
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(verts));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return m;
}
private Material makeMat(ColorRGBA col) {
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);
private void ensureAssets() {
Path root = AnimationLibrary.findAssetRoot();
Path dir = root.resolve("Textures").resolve("hud");
try {
Files.createDirectories(dir);
} catch (IOException e) {
log.warn("[Compass] Verzeichnis nicht erstellbar: {}", dir);
return;
}
return m;
ensureRose(dir.resolve("compass_rose.png"));
ensureNeedle(dir.resolve("compass_needle.png"));
}
private BitmapText makeBitmapText(String text, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setColor(col.clone());
t.setText(text);
t.setQueueBucket(RenderQueue.Bucket.Gui);
return t;
/** Erzeugt Platzhalter-Rose: dunkler Kreis + 8 Striche + N/E/S/W. */
private static void ensureRose(Path path) {
if (Files.exists(path)) { return; }
try {
int cx = 50, cy = 50, r = 47;
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// Hintergrund-Kreis
g.setColor(new Color(12, 12, 24, 209));
g.fillOval(cx - r, cy - r, r * 2, r * 2);
// Rand-Ring
g.setColor(new Color(100, 100, 160, 216));
g.setStroke(new BasicStroke(1.5f));
g.drawOval(cx - r, cy - r, r * 2, r * 2);
// 8 Tick-Striche; angle 90° → 0° zeigt nach oben (= Norden in BufferedImage-Y)
for (int i = 0; i < 8; i++) {
double angle = Math.toRadians(i * 45.0 - 90.0);
boolean major = (i % 2 == 0);
int innerR = r - (major ? 9 : 5);
int x1 = (int)(cx + Math.cos(angle) * innerR);
int y1 = (int)(cy + Math.sin(angle) * innerR);
int x2 = (int)(cx + Math.cos(angle) * (r - 1));
int y2 = (int)(cy + Math.sin(angle) * (r - 1));
g.setColor(new Color(160, 160, 200, 166));
g.setStroke(new BasicStroke(major ? 1.5f : 1.0f));
g.drawLine(x1, y1, x2, y2);
}
// Himmelsrichtungs-Buchstaben
int labelR = 31;
g.setFont(new Font("SansSerif", Font.BOLD, 13));
FontMetrics fm = g.getFontMetrics();
g.setColor(new Color(255, 80, 80, 255));
drawCentered(g, fm, "N", cx, cy - labelR); // oben in BufferedImage = oben auf dem Bildschirm
g.setFont(new Font("SansSerif", Font.BOLD, 11));
fm = g.getFontMetrics();
g.setColor(new Color(210, 210, 230, 255));
drawCentered(g, fm, "E", cx + labelR, cy);
drawCentered(g, fm, "S", cx, cy + labelR);
drawCentered(g, fm, "W", cx - labelR, cy);
g.dispose();
ImageIO.write(img, "PNG", path.toFile());
} catch (Exception e) {
log.warn("[Compass] compass_rose.png nicht erstellbar: {}", e.getMessage());
}
}
/** Erzeugt Platzhalter-Nadel: gelbes Dreieck, Spitze oben. */
private static void ensureNeedle(Path path) {
if (Files.exists(path)) { return; }
try {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(new Color(255, 220, 60, 242));
// Spitze bei y=8, Basis bei y=21; kleines y in BufferedImage = oben auf dem Bildschirm
int[] xp = {50, 43, 57};
int[] yp = { 8, 21, 21};
g.fillPolygon(xp, yp, 3);
g.dispose();
ImageIO.write(img, "PNG", path.toFile());
} catch (Exception e) {
log.warn("[Compass] compass_needle.png nicht erstellbar: {}", e.getMessage());
}
}
private static void drawCentered(Graphics2D g, FontMetrics fm, String text, int cx, int cy) {
int x = cx - fm.stringWidth(text) / 2;
int y = cy + (fm.getAscent() - fm.getDescent()) / 2;
g.drawString(text, x, y);
}
}

View File

@@ -4,6 +4,8 @@ import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.PhysicsRayTestResult;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
@@ -201,6 +203,8 @@ public class GrassState extends BaseAppState
List<GrassTuft> tufts = chunkTufts[idx];
if (tufts.isEmpty()) return;
BulletAppState bullet = getApplication().getStateManager().getState(BulletAppState.class);
Map<Integer, List<float[]>> bySlot = new LinkedHashMap<>();
for (GrassTuft t : tufts) {
long seed = (long) Float.floatToRawIntBits(t.x()) * 0x9E3779B9L
@@ -210,7 +214,7 @@ public class GrassState extends BaseAppState
for (int b = 0; b < BLADES_PER_TUFT; b++) {
float bx = t.x() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f;
float bz = t.z() + (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f;
float th = terrainChunkState.getHeightAt(bx, bz);
float th = surfaceHeight(bullet, bx, bz);
if (Float.isNaN(th)) continue;
float h = t.height() * (0.7f + rng.nextFloat() * 0.6f);
blades.add(new float[]{bx, th, bz, h});
@@ -259,12 +263,36 @@ public class GrassState extends BaseAppState
chunkNodes[ci].setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
// ── Oberflächen-Höhe via Physik-Raycast ──────────────────────────────────
private float surfaceHeight(BulletAppState bullet, float wx, float wz) {
float base = terrainChunkState.getHeightAt(wx, wz);
if (bullet == null || bullet.getPhysicsSpace() == null) {
return base;
}
float refY = Float.isNaN(base) ? 0f : base;
Vector3f from = new Vector3f(wx, refY + 200f, wz);
Vector3f to = new Vector3f(wx, refY - 5f, wz);
List<PhysicsRayTestResult> hits = bullet.getPhysicsSpace().rayTest(from, to);
if (hits.isEmpty()) {
return base;
}
float bestFrac = Float.MAX_VALUE;
for (PhysicsRayTestResult hit : hits) {
if (hit.getHitFraction() < bestFrac) {
bestFrac = hit.getHitFraction();
}
}
return from.y + (to.y - from.y) * bestFrac;
}
// ── Mesh: Kreuz-Quad mit UV ───────────────────────────────────────────────
private static Mesh buildGrassMesh(List<float[]> blades) {
int n = blades.size();
FloatBuffer pos = BufferUtils.createFloatBuffer(n * 8 * 3);
FloatBuffer uv = BufferUtils.createFloatBuffer(n * 8 * 2);
FloatBuffer nrm = BufferUtils.createFloatBuffer(n * 8 * 3);
IntBuffer idx = BufferUtils.createIntBuffer(n * 12);
int vi = 0;
@@ -272,15 +300,15 @@ public class GrassState extends BaseAppState
float x = blade[0], y = blade[1], z = blade[2], h = blade[3];
float w = Math.max(0.05f, h * BLADE_WIDTH);
pos.put(x-w).put(y ).put(z); uv.put(0).put(0);
pos.put(x+w).put(y ).put(z); uv.put(1).put(0);
pos.put(x+w).put(y+h).put(z); uv.put(1).put(1);
pos.put(x-w).put(y+h).put(z); uv.put(0).put(1);
pos.put(x-w).put(y ).put(z); uv.put(0).put(0); nrm.put(0).put(1).put(0);
pos.put(x+w).put(y ).put(z); uv.put(1).put(0); nrm.put(0).put(1).put(0);
pos.put(x+w).put(y+h).put(z); uv.put(1).put(1); nrm.put(0).put(1).put(0);
pos.put(x-w).put(y+h).put(z); uv.put(0).put(1); nrm.put(0).put(1).put(0);
pos.put(x).put(y ).put(z-w); uv.put(0).put(0);
pos.put(x).put(y ).put(z+w); uv.put(1).put(0);
pos.put(x).put(y+h).put(z+w); uv.put(1).put(1);
pos.put(x).put(y+h).put(z-w); uv.put(0).put(1);
pos.put(x).put(y ).put(z-w); uv.put(0).put(0); nrm.put(0).put(1).put(0);
pos.put(x).put(y ).put(z+w); uv.put(1).put(0); nrm.put(0).put(1).put(0);
pos.put(x).put(y+h).put(z+w); uv.put(1).put(1); nrm.put(0).put(1).put(0);
pos.put(x).put(y+h).put(z-w); uv.put(0).put(1); nrm.put(0).put(1).put(0);
idx.put(vi ).put(vi+1).put(vi+2);
idx.put(vi ).put(vi+2).put(vi+3);
@@ -292,6 +320,7 @@ public class GrassState extends BaseAppState
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv);
mesh.setBuffer(VertexBuffer.Type.Normal, 3, nrm);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
return mesh;

View File

@@ -21,6 +21,8 @@ import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import de.blight.common.model.Item;
import de.blight.common.model.MainCharacter;
import de.blight.game.config.MenuScreen;
import de.blight.game.config.OverlayState;
/**
* Immer sichtbare Schnellzugriffsleiste (Hotbar) mit 10 Slots (Tasten 19, 0).
@@ -60,8 +62,9 @@ public class HotbarState extends BaseAppState {
private Node hotbarNode;
private float originX, originY;
final Item[] slots = new Item[SLOT_COUNT];
private int activeSlot = 0;
final Item[] slots = new Item[SLOT_COUNT];
private int activeSlot = 0;
private boolean lastMenuOpen = false;
private final Geometry[] borderGeoms = new Geometry[SLOT_COUNT];
private final Node[] thumbNodes = new Node[SLOT_COUNT];
@@ -98,6 +101,17 @@ public class HotbarState extends BaseAppState {
@Override
protected void cleanup(Application application) {}
@Override
public void update(float tpf) {
boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen();
if (menuOpen != lastMenuOpen) {
lastMenuOpen = menuOpen;
if (hotbarNode != null) {
hotbarNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit);
}
}
}
// ── Aufbau ───────────────────────────────────────────────────────────────
private void buildHotbar() {
float sw = app.getCamera().getWidth();

View File

@@ -17,6 +17,8 @@ import com.jme3.ui.Picture;
import com.jme3.util.BufferUtils;
import de.blight.common.model.MainCharacter;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.MenuScreen;
import de.blight.game.config.OverlayState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -62,7 +64,8 @@ public class HudState extends BaseAppState {
private SimpleApplication app;
private Node hudNode;
private final Geometry[] fills = new Geometry[3];
private final float[] lastRatios = { -1f, -1f, -1f };
private final float[] lastRatios = { -1f, -1f, -1f };
private boolean lastMenuOpen = false;
public HudState(MainCharacter mc) {
this.mc = mc;
@@ -96,6 +99,13 @@ public class HudState extends BaseAppState {
@Override
public void update(float tpf) {
boolean menuOpen = OverlayState.isAnyOpen() || MenuScreen.isAnyOpen();
if (menuOpen != lastMenuOpen) {
lastMenuOpen = menuOpen;
if (hudNode != null) {
hudNode.setCullHint(menuOpen ? Spatial.CullHint.Always : Spatial.CullHint.Inherit);
}
}
if (mc == null) return;
int maxHp = mc.getMaxHp();
int maxSt = mc.getMaxStamina();

View File

@@ -74,6 +74,8 @@ public class SculptedMeshState extends BaseAppState {
ColorRGBA ac = dns.getCustomAmbient();
material.setVector3("SunColor", new Vector3f(sc.r, sc.g, sc.b));
material.setVector3("AmbientColor", new Vector3f(ac.r, ac.g, ac.b));
RainState rs = app.getStateManager().getState(RainState.class);
if (rs != null) material.setFloat("Wetness", rs.getWetness());
}
// ── Laden ────────────────────────────────────────────────────────────────