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

@@ -0,0 +1,307 @@
// Blight-Override von JME3's Shadows.glsllib
// Änderung: SHADOW_BIAS wird einmal in getDirectionalLightShadows() von
// projCoord.z abgezogen (Empfänger-seitiger Depth-Bias).
// Das behebt Self-Shadowing-Acne bei hohem Sonnenstand, ohne den
// Pre-Shadow-PolyOffset zu erhöhen.
// Wert anpassen: bei verbleibendem Acne erhöhen, bei "Peter Panning" verkleinern.
#define SHADOW_BIAS 0.0002
#if __VERSION__ >= 130
#ifdef GL_ES
#extension GL_OES_gpu_shader5 : enable
#else
#extension GL_ARB_gpu_shader5 : enable
#endif
#define IVEC2 ivec2
#ifdef HARDWARE_SHADOWS
#define SHADOWMAP sampler2DShadow
#define SHADOWCOMPAREOFFSET(tex,coord,offset) textureProjOffset(tex, coord, offset)
#define SHADOWCOMPARE(tex,coord) textureProj(tex, coord)
#if defined GL_ES && __VERSION__ <= 300
#define SHADOWGATHER(tex,coord) step(coord.z, textureGather(tex, coord.xy))
#else
#define SHADOWGATHER(tex,coord) textureGather(tex, coord.xy, coord.z)
#endif
#else
#define SHADOWMAP sampler2D
#define SHADOWCOMPAREOFFSET(tex,coord,offset) step(coord.z, textureProjOffset(tex, coord, offset).r)
#define SHADOWCOMPARE(tex,coord) step(coord.z, textureProj(tex, coord).r)
#define SHADOWGATHER(tex,coord) step(coord.z, textureGather(tex, coord.xy))
#endif
#if FILTER_MODE == 10
#define GETSHADOW Shadow_Nearest
#define KERNEL 1.0
#elif FILTER_MODE == 1
#ifdef HARDWARE_SHADOWS
#define GETSHADOW Shadow_Nearest
#else
#define GETSHADOW Shadow_DoBilinear_2x2
#endif
#define KERNEL 1.0
#endif
#else
#define IVEC2 vec2
#if defined GL_ES
#define SHADOWMAP sampler2D
#define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r)
#elif defined HARDWARE_SHADOWS
#define SHADOWMAP sampler2DShadow
#define SHADOWCOMPARE(tex,coord) shadow2DProj(tex, coord).r
#else
#define SHADOWMAP sampler2D
#define SHADOWCOMPARE(tex,coord) step(coord.z, texture2DProj(tex, coord).r)
#endif
#if FILTER_MODE == 10
#define GETSHADOW Shadow_DoShadowCompare
#define KERNEL 1.0
#elif FILTER_MODE == 1
#ifdef HARDWARE_SHADOWS
#define GETSHADOW Shadow_DoShadowCompare
#else
#define GETSHADOW Shadow_DoBilinear_2x2
#endif
#define KERNEL 1.0
#endif
#endif
#if (FILTER_MODE == 2)
#define GETSHADOW Shadow_DoDither_2x2
#define KERNEL 1.0
#elif FILTER_MODE == 3
#define GETSHADOW Shadow_DoPCF
#define KERNEL 4.0
#elif FILTER_MODE == 4
#define GETSHADOW Shadow_DoPCFPoisson
#define KERNEL 4.0
#elif FILTER_MODE == 5
#define GETSHADOW Shadow_DoPCF
#define KERNEL 8.0
#endif
uniform SHADOWMAP m_ShadowMap0;
uniform SHADOWMAP m_ShadowMap1;
uniform SHADOWMAP m_ShadowMap2;
uniform SHADOWMAP m_ShadowMap3;
#ifdef POINTLIGHT
uniform SHADOWMAP m_ShadowMap4;
uniform SHADOWMAP m_ShadowMap5;
#endif
#ifdef PSSM
uniform vec4 m_Splits;
#endif
uniform float m_ShadowIntensity;
const vec2 pixSize2 = vec2(1.0 / SHADOWMAP_SIZE);
float shadowBorderScale = 1.0;
float Shadow_DoShadowCompare(in SHADOWMAP tex,in vec4 projCoord){
return SHADOWCOMPARE(tex, projCoord);
}
float Shadow_BorderCheck(in vec2 coord){
#ifdef GL_ES
return 0.0;
#else
vec4 t = vec4(coord.xy, 0.0, 1.0);
t = step(t.wwxy, t.xyzz);
return dot(t,t);
#endif
}
float Shadow_Nearest(in SHADOWMAP tex,in vec4 projCoord){
float border = Shadow_BorderCheck(projCoord.xy);
if (border > 0.0){
return 1.0;
}
return SHADOWCOMPARE(tex, projCoord);
}
float Shadow_DoShadowCompareOffset(in SHADOWMAP tex,in vec4 projCoord,in vec2 offset){
vec4 coord = vec4(projCoord.xy + offset.xy * pixSize2 * shadowBorderScale, projCoord.zw);
return SHADOWCOMPARE(tex, coord);
}
float Shadow_DoDither_2x2(in SHADOWMAP tex, in vec4 projCoord){
float border = Shadow_BorderCheck(projCoord.xy);
if (border > 0.0)
return 1.0;
float shadow = 0.0;
vec2 o = vec2(IVEC2(mod(floor(gl_FragCoord.xy), 2.0)));
shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2(-1.5, 1.5)+o));
shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2( 0.5, 1.5)+o));
shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2(-1.5, -0.5)+o));
shadow += Shadow_DoShadowCompareOffset(tex, projCoord, (vec2( 0.5, -0.5)+o));
shadow *= 0.25;
return shadow;
}
float Shadow_DoBilinear_2x2(in SHADOWMAP tex, in vec4 projCoord){
float border = Shadow_BorderCheck(projCoord.xy);
if (border > 0.0){
return 1.0;
}
vec4 gather = vec4(0.0);
#if __VERSION__ >= 130
#if defined GL_ARB_gpu_shader5 || defined GL_OES_gpu_shader5
vec4 coord = vec4(projCoord.xyz / projCoord.www,0.0);
gather = SHADOWGATHER(tex, coord);
#else
gather.x = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 1));
gather.y = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 1));
gather.z = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(1, 0));
gather.w = SHADOWCOMPAREOFFSET(tex, projCoord, ivec2(0, 0));
#endif
#else
gather.x = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 0.0));
gather.y = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 0.0));
gather.z = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(0.0, 1.0));
gather.w = Shadow_DoShadowCompareOffset(tex, projCoord, vec2(1.0, 1.0));
#endif
vec2 f = fract( projCoord.xy * SHADOWMAP_SIZE );
vec2 mx = mix( gather.wx, gather.zy, f.x );
return mix( mx.x, mx.y, f.y );
}
float Shadow_DoPCF(in SHADOWMAP tex,in vec4 projCoord){
float shadow = 0.0;
float border = Shadow_BorderCheck(projCoord.xy);
if (border > 0.0)
return 1.0;
const float bound = (KERNEL * 0.5 - 0.5 ) * PCFEDGE;
for (float y = -bound; y <= bound; y += PCFEDGE){
for (float x = -bound; x <= bound; x += PCFEDGE){
#if __VERSION__ < 130
shadow += clamp(Shadow_DoShadowCompareOffset(tex,projCoord,vec2(x,y)) + border, 0.0, 1.0);
#else
shadow += Shadow_DoShadowCompareOffset(tex, projCoord, vec2(x,y));
#endif
}
}
shadow = shadow / (KERNEL * KERNEL);
return shadow;
}
//12 tap poisson disk
const vec2 poissonDisk0 = vec2(-0.1711046, -0.425016);
const vec2 poissonDisk1 = vec2(-0.7829809, 0.2162201);
const vec2 poissonDisk2 = vec2(-0.2380269, -0.8835521);
const vec2 poissonDisk3 = vec2(0.4198045, 0.1687819);
const vec2 poissonDisk4 = vec2(-0.684418, -0.3186957);
const vec2 poissonDisk5 = vec2(0.6026866, -0.2587841);
const vec2 poissonDisk6 = vec2(-0.2412762, 0.3913516);
const vec2 poissonDisk7 = vec2(0.4720655, -0.7664126);
const vec2 poissonDisk8 = vec2(0.9571564, 0.2680693);
const vec2 poissonDisk9 = vec2(-0.5238616, 0.802707);
const vec2 poissonDisk10 = vec2(0.5653144, 0.60262);
const vec2 poissonDisk11 = vec2(0.0123658, 0.8627419);
float Shadow_DoPCFPoisson(in SHADOWMAP tex, in vec4 projCoord){
float shadow = 0.0;
float border = Shadow_BorderCheck(projCoord.xy);
if (border > 0.0){
return 1.0;
}
vec2 texelSize = pixSize2 * 4.0 * PCFEDGE * shadowBorderScale;
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk0 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk1 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk2 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk3 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk4 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk5 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk6 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk7 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk8 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk9 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk10 * texelSize, projCoord.zw));
shadow += SHADOWCOMPARE(tex, vec4(projCoord.xy + poissonDisk11 * texelSize, projCoord.zw));
//this is divided by 12
return shadow * 0.08333333333;
}
#ifdef POINTLIGHT
float getPointLightShadows(in vec4 worldPos,in vec3 lightPos,
in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,in SHADOWMAP shadowMap4,in SHADOWMAP shadowMap5,
in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3,in vec4 projCoord4,in vec4 projCoord5){
float shadow = 1.0;
vec3 vect = worldPos.xyz - lightPos;
vec3 absv= abs(vect);
float maxComp = max(absv.x,max(absv.y,absv.z));
if(maxComp == absv.y){
if(vect.y < 0.0){
shadow = GETSHADOW(shadowMap0, projCoord0 / projCoord0.w);
}else{
shadow = GETSHADOW(shadowMap1, projCoord1 / projCoord1.w);
}
}else if(maxComp == absv.z){
if(vect.z < 0.0){
shadow = GETSHADOW(shadowMap2, projCoord2 / projCoord2.w);
}else{
shadow = GETSHADOW(shadowMap3, projCoord3 / projCoord3.w);
}
}else if(maxComp == absv.x){
if(vect.x < 0.0){
shadow = GETSHADOW(shadowMap4, projCoord4 / projCoord4.w);
}else{
shadow = GETSHADOW(shadowMap5, projCoord5 / projCoord5.w);
}
}
return shadow;
}
#else
#ifdef PSSM
float getDirectionalLightShadows(in vec4 splits,in float shadowPosition,
in SHADOWMAP shadowMap0,in SHADOWMAP shadowMap1,in SHADOWMAP shadowMap2,in SHADOWMAP shadowMap3,
in vec4 projCoord0,in vec4 projCoord1,in vec4 projCoord2,in vec4 projCoord3){
// Receiver-seitiger Depth-Bias: verhindert Self-Shadowing-Acne bei
// flachen Oberflächen und hohem Sonnenstand (PSSM nutzt orthografische
// Projektion → w=1, daher direkt von z abziehen).
vec4 pc0 = vec4(projCoord0.xy, projCoord0.z - SHADOW_BIAS, projCoord0.w);
vec4 pc1 = vec4(projCoord1.xy, projCoord1.z - SHADOW_BIAS, projCoord1.w);
vec4 pc2 = vec4(projCoord2.xy, projCoord2.z - SHADOW_BIAS, projCoord2.w);
vec4 pc3 = vec4(projCoord3.xy, projCoord3.z - SHADOW_BIAS, projCoord3.w);
float shadow = 1.0;
if(shadowPosition < splits.x){
shadow = GETSHADOW(shadowMap0, pc0);
}else if( shadowPosition < splits.y){
shadowBorderScale = 0.5;
shadow = GETSHADOW(shadowMap1, pc1);
}else if( shadowPosition < splits.z){
shadowBorderScale = 0.25;
shadow = GETSHADOW(shadowMap2, pc2);
}else if( shadowPosition < splits.w){
shadowBorderScale = 0.125;
shadow = GETSHADOW(shadowMap3, pc3);
}
return shadow;
}
#else
float getSpotLightShadows(in SHADOWMAP shadowMap,in vec4 projCoord){
float shadow = 1.0;
projCoord /= projCoord.w;
shadow = GETSHADOW(shadowMap,projCoord);
projCoord = projCoord * 2.0 - 1.0;
float fallOff = ( length(projCoord.xy) - 0.9 ) / 0.1;
return mix(shadow,1.0,clamp(fallOff,0.0,1.0));
}
#endif
#endif

View File

@@ -12,6 +12,7 @@ MaterialDef BakedTerrain {
Vector3 SunColor : 0.68 0.65 0.60
Vector3 AmbientColor : 0.08 0.10 0.16
Boolean DebugNoLight
Float Wetness
}
Technique {

View File

@@ -17,6 +17,7 @@ uniform vec3 m_LightDir;
uniform vec3 m_SunColor;
uniform vec3 m_AmbientColor;
uniform vec3 g_CameraPosition;
uniform float m_Wetness;
in vec3 vWorldPos;
in vec3 vNormal;
@@ -82,10 +83,17 @@ void main() {
float diff = max(dot(N, lightDir), 0.0);
vec3 light = m_AmbientColor + m_SunColor * diff;
// Nass-Effekt: Oberfläche abdunkeln + Blinn-Phong Specular Schimmer
float wet = clamp(m_Wetness, 0.0, 1.0);
col.rgb *= (1.0 - 0.20 * wet);
vec3 viewDir = normalize(g_CameraPosition - vWorldPos);
vec3 H = normalize(lightDir + viewDir);
float spec = pow(max(dot(N, H), 0.0), 80.0) * wet;
const float BRIGHTNESS = 0.80;
#ifdef DEBUG_NO_LIGHT
outColor = vec4(col.rgb * BRIGHTNESS, col.a);
#else
outColor = vec4(col.rgb * light * BRIGHTNESS, col.a);
outColor = vec4(col.rgb * light * BRIGHTNESS + vec3(spec * 0.35), col.a);
#endif
}

View File

@@ -29,13 +29,15 @@ void main() {
vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0);
float wavePhase = dot(worldXZ, windN);
// Pro-Halm-Zufallsphase: floor() macht sie für alle Vertices desselben Halms gleich
float randPhase = fract(sin(dot(floor(worldXZ), vec2(127.1, 311.7))) * 43758.5453) * 6.2832;
// Zwei überlagerte Sinuswellen für organische Bewegung
float sway = sin(t * 2.1 + wavePhase * 0.08) * 0.6
+ sin(t * 1.4 + wavePhase * 0.06) * 0.4;
// Zwei überlagerte Sinuswellen + Zufallsphase → kein synchroner Gleichtakt
float sway = sin(t * 2.1 + wavePhase * 0.08 + 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);
// Halbierte Windstärke für Textur-Gras; Mindest-Stärke für minimale Bewegung
float effectiveStrength = max(m_WindStrength, 0.05) * 0.5;
// Quadratische Gewichtung: Spitze bewegt sich mehr als Basis
float bend = sway * effectiveStrength * inTexCoord.y * inTexCoord.y;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -290,7 +290,8 @@ public class PlacedObjectState extends BaseAppState {
}
private void paintGrass(float cx, float cz, float radius) {
int n = Math.max(1, (int) input.grassTool.density.getValue());
int n = (int) Math.round(input.grassTool.density.getValue() * radius * 2.0);
if (n <= 0) return;
float baseH = (float) input.grassTool.grassHeight.getValue();
int slot = input.grassActiveSlot;
Random rng = new Random();
@@ -423,13 +424,13 @@ public class PlacedObjectState extends BaseAppState {
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);
for (int i = 0; i < 4; i++) { nor.put(0).put(0).put(1); tan.put(1).put(0).put(0).put(1); }
// Quad 2: liegt in ZY-Ebene (konstantes x) Normal zeigt -X, Tangent zeigt +Z
for (int i = 0; i < 4; i++) { nor.put(0).put(1).put(0); tan.put(1).put(0).put(0).put(1); }
// Quad 2: liegt in ZY-Ebene (konstantes x) Normal zeigt +Y, Tangent zeigt +Z
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);
for (int i = 0; i < 4; i++) { nor.put(-1).put(0).put(0); tan.put(0).put(0).put(1).put(1); }
for (int i = 0; i < 4; i++) { nor.put(0).put(1).put(0); tan.put(0).put(0).put(1).put(1); }
idx.put(vi).put(vi+1).put(vi+2);
idx.put(vi).put(vi+2).put(vi+3);
idx.put(vi+4).put(vi+5).put(vi+6);

View File

@@ -7,9 +7,9 @@ import java.util.List;
*/
public class GrassTool extends EditorTool {
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 40.0, 1.0, 500.0);
public final ToolParameter grassHeight = new ToolParameter("Grashöhe", 1.5, 0.1, 10.0);
public final ToolParameter density = new ToolParameter("Dichte", 8.0, 1.0, 200.0);
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
public final ToolParameter grassHeight = new ToolParameter("Grashöhe", 0.8, 0.0, 2.0);
public final ToolParameter density = new ToolParameter("Dichte", 0.5, 0.0, 2.0);
@Override public String getName() { return "Gras (Textur)"; }

View File

@@ -340,11 +340,12 @@ public class BlightGame extends SimpleApplication {
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);
}
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 final Node[] labelNodes = new Node[4];
private final Quaternion roseRot = new Quaternion();
private final Quaternion labelRot = 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;
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;
}
ensureRose(dir.resolve("compass_rose.png"));
ensureNeedle(dir.resolve("compass_needle.png"));
}
/**
* 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;
/** 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);
}
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;
// 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());
}
}
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);
/** 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());
}
return m;
}
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;
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).
@@ -62,6 +64,7 @@ public class HotbarState extends BaseAppState {
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;
@@ -63,6 +65,7 @@ public class HudState extends BaseAppState {
private Node hudNode;
private final Geometry[] fills = new Geometry[3];
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 ────────────────────────────────────────────────────────────────