Compare commits

...

2 Commits

Author SHA1 Message Date
51b1b53a27 Dialog Camera und DoF bearbeitet 2026-07-21 22:26:06 +02:00
ae244e3969 Weitere Grafik Filter hinzugeüfgt 2026-07-21 15:25:17 +02:00
8 changed files with 201 additions and 43 deletions

View File

@@ -30,7 +30,8 @@ public class GraphicsScreen extends MenuScreen {
private static final int ROW_AA = 3; private static final int ROW_AA = 3;
private static final int ROW_VIEWDIST = 4; private static final int ROW_VIEWDIST = 4;
private static final int ROW_SHADOW = 5; private static final int ROW_SHADOW = 5;
private static final int ROW_COUNT = 6; private static final int ROW_POSTFX = 6;
private static final int ROW_COUNT = 7;
private final GraphicsSettings live; private final GraphicsSettings live;
private GraphicsSettings edit; private GraphicsSettings edit;
@@ -63,6 +64,7 @@ public class GraphicsScreen extends MenuScreen {
edit.samples = live.samples; edit.samples = live.samples;
edit.viewDistance = live.viewDistance; edit.viewDistance = live.viewDistance;
edit.shadowQuality = live.shadowQuality; edit.shadowQuality = live.shadowQuality;
edit.postFxQuality = live.postFxQuality;
resIdx = 0; resIdx = 0;
for (int i = 0; i < resolutions.length; i++) { for (int i = 0; i < resolutions.length; i++) {
@@ -86,7 +88,8 @@ public class GraphicsScreen extends MenuScreen {
"menu.graphics.row.vsync", "menu.graphics.row.vsync",
"menu.graphics.row.aa", "menu.graphics.row.aa",
"menu.graphics.row.viewdist", "menu.graphics.row.viewdist",
"menu.graphics.row.shadow" "menu.graphics.row.shadow",
"menu.graphics.row.postfx"
}; };
float lblX = CONT_X + 30f; float lblX = CONT_X + 30f;
@@ -145,6 +148,11 @@ public class GraphicsScreen extends MenuScreen {
case MEDIUM -> t("menu.graphics.val.shadow.medium"); case MEDIUM -> t("menu.graphics.val.shadow.medium");
case HIGH -> t("menu.graphics.val.shadow.high"); case HIGH -> t("menu.graphics.val.shadow.high");
}; };
case ROW_POSTFX -> switch (edit.postFxQuality) {
case OFF -> t("menu.graphics.val.off");
case MEDIUM -> t("menu.graphics.val.shadow.medium");
case HIGH -> t("menu.graphics.val.shadow.high");
};
default -> ""; default -> "";
}; };
BitmapText vt = valTexts[row]; BitmapText vt = valTexts[row];
@@ -178,18 +186,24 @@ public class GraphicsScreen extends MenuScreen {
GraphicsSettings.ShadowQuality[] sqs = GraphicsSettings.ShadowQuality.values(); GraphicsSettings.ShadowQuality[] sqs = GraphicsSettings.ShadowQuality.values();
edit.shadowQuality = sqs[(edit.shadowQuality.ordinal() + dir + sqs.length) % sqs.length]; edit.shadowQuality = sqs[(edit.shadowQuality.ordinal() + dir + sqs.length) % sqs.length];
break; break;
case ROW_POSTFX:
GraphicsSettings.PostFxQuality[] pfqs = GraphicsSettings.PostFxQuality.values();
edit.postFxQuality = pfqs[(edit.postFxQuality.ordinal() + dir + pfqs.length) % pfqs.length];
break;
} }
refreshText(row); refreshText(row);
} }
private void applyAndSave() { private void applyAndSave() {
boolean shadowChanged = live.shadowQuality != edit.shadowQuality; boolean shadowChanged = live.shadowQuality != edit.shadowQuality;
boolean postFxChanged = live.postFxQuality != edit.postFxQuality;
boolean needsRestart = live.width != edit.width boolean needsRestart = live.width != edit.width
|| live.height != edit.height || live.height != edit.height
|| live.fullscreen != edit.fullscreen || live.fullscreen != edit.fullscreen
|| live.vsync != edit.vsync || live.vsync != edit.vsync
|| live.samples != edit.samples || live.samples != edit.samples
|| shadowChanged; || shadowChanged
|| postFxChanged;
live.width = edit.width; live.width = edit.width;
live.height = edit.height; live.height = edit.height;
@@ -198,6 +212,7 @@ public class GraphicsScreen extends MenuScreen {
live.samples = edit.samples; live.samples = edit.samples;
live.viewDistance = edit.viewDistance; live.viewDistance = edit.viewDistance;
live.shadowQuality = edit.shadowQuality; live.shadowQuality = edit.shadowQuality;
live.postFxQuality = edit.postFxQuality;
GraphicsStore.save(live); GraphicsStore.save(live);
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class); WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);

View File

@@ -10,6 +10,29 @@ public class GraphicsSettings {
public ShadowQuality shadowQuality = ShadowQuality.MEDIUM; public ShadowQuality shadowQuality = ShadowQuality.MEDIUM;
/** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */ /** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */
public boolean debugLod2 = false; public boolean debugLod2 = false;
public PostFxQuality postFxQuality = PostFxQuality.HIGH;
public enum PostFxQuality {
// fxaa toneMap bloomI bloomExp godRays
OFF ( false, false, 0f, 0f, false),
MEDIUM( true, true, 0.4f, 3.0f, false),
HIGH ( true, true, 0.6f, 3.5f, true );
public final boolean fxaa;
public final boolean toneMap;
public final float bloomIntensity;
public final float bloomExposurePower;
public final boolean godRays;
PostFxQuality(boolean fxaa, boolean toneMap,
float bloomIntensity, float bloomExposurePower, boolean godRays) {
this.fxaa = fxaa;
this.toneMap = toneMap;
this.bloomIntensity = bloomIntensity;
this.bloomExposurePower = bloomExposurePower;
this.godRays = godRays;
}
}
/** /**
* Schattenqualitäts-Stufen. * Schattenqualitäts-Stufen.

View File

@@ -23,7 +23,7 @@ public class ThirdPersonCamera {
private static final float BASE_DISTANCE = 5f; private static final float BASE_DISTANCE = 5f;
private static final float MIN_DISTANCE = BASE_DISTANCE - 3f; private static final float MIN_DISTANCE = BASE_DISTANCE - 3f;
private static final float MAX_DISTANCE = BASE_DISTANCE + 3f; private static final float MAX_DISTANCE = BASE_DISTANCE + 3f;
private static final float MIN_VERTICAL_ANGLE = 0.08f; // Kamera immer leicht über Schulter private static final float MIN_VERTICAL_ANGLE = -0.12f; // Kamera kann bis Kniehöhe sinken
private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f; private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
private static final float TARGET_HEIGHT = 1.0f; private static final float TARGET_HEIGHT = 1.0f;
@@ -38,6 +38,7 @@ public class ThirdPersonCamera {
private float distance = BASE_DISTANCE; private float distance = BASE_DISTANCE;
private boolean paused = false; private boolean paused = false;
public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) { public ThirdPersonCamera(Camera cam, InputManager inputManager, KeyBindings keyBindings) {
this.cam = cam; this.cam = cam;
this.inputManager = inputManager; this.inputManager = inputManager;

View File

@@ -80,6 +80,8 @@ public class WorldScene extends BaseAppState {
private MapData loadedMapData; private MapData loadedMapData;
private FilterPostProcessor sharedFPP; private FilterPostProcessor sharedFPP;
private BlightFogFilter fogFilter; private BlightFogFilter fogFilter;
private com.jme3.post.filters.LightScatteringFilter lightScatterFilter;
private com.jme3.post.filters.DepthOfFieldFilter dofFilter;
private TerrainChunkState terrainChunkState; private TerrainChunkState terrainChunkState;
private Material terrainMaterial; private Material terrainMaterial;
@@ -435,6 +437,15 @@ public class WorldScene extends BaseAppState {
for (Material mat : characterPbrMaterials) { for (Material mat : characterPbrMaterials) {
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f)); mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
} }
if (lightScatterFilter != null) {
Vector3f sunDir = dayNight.getSunDirection();
// Weltposition der Sonne: von der Kamera aus weit entfernt entgegen der Lichtrichtung
Vector3f sunWorld = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
lightScatterFilter.setLightPosition(sunWorld);
// Deaktivieren wenn Sonne hinter der Kamera (gleiche Richtung wie Kamera schaut)
lightScatterFilter.setEnabled(sunDir.dot(app.getCamera().getDirection()) < 0.2f);
}
} }
} }
@@ -492,6 +503,25 @@ public class WorldScene extends BaseAppState {
log.info("[Debug] Nebel {}", nowEnabled ? "AN" : "AUS"); log.info("[Debug] Nebel {}", nowEnabled ? "AN" : "AUS");
} }
/**
* Tiefenschärfe aktivieren. focusRange definiert den beidseitigen Schärfebereich
* um focusDistance — alles innerhalb [focusDistance-focusRange, focusDistance+focusRange]
* bleibt scharf, der Rest wird geblurt.
*/
public void focusDof(float focusDistance, float focusRange, float blurScale) {
if (dofFilter == null) return;
dofFilter.setFocusDistance(focusDistance);
dofFilter.setFocusRange(focusRange);
dofFilter.setBlurScale(blurScale);
dofFilter.setEnabled(true);
}
/** Tiefenschärfe deaktivieren. */
public void clearDof() {
if (dofFilter == null) return;
dofFilter.setEnabled(false);
}
@Override protected void cleanup(Application app) { @Override protected void cleanup(Application app) {
} }
@Override protected void onDisable() {} @Override protected void onDisable() {}
@@ -778,6 +808,43 @@ public class WorldScene extends BaseAppState {
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage()); log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());
} }
GraphicsSettings.PostFxQuality pfx = graphicsSettings.postFxQuality;
if (pfx.bloomIntensity > 0f) {
com.jme3.post.filters.BloomFilter bloom =
new com.jme3.post.filters.BloomFilter(com.jme3.post.filters.BloomFilter.GlowMode.Scene);
bloom.setBloomIntensity(pfx.bloomIntensity);
bloom.setExposurePower(pfx.bloomExposurePower);
bloom.setDownSamplingFactor(2f);
fpp.addFilter(bloom);
}
if (pfx.godRays) {
// Initiale Sonnenposition: von Kamera aus weit entfernt entgegen Lichtrichtung
Vector3f initSunPos = app.getCamera().getLocation().add(sunDir.negate().mult(10000f));
lightScatterFilter = new com.jme3.post.filters.LightScatteringFilter(initSunPos);
lightScatterFilter.setLightDensity(0.8f);
lightScatterFilter.setBlurStart(0.3f);
lightScatterFilter.setBlurWidth(0.4f);
lightScatterFilter.setNbSamples(150);
fpp.addFilter(lightScatterFilter);
}
if (pfx.toneMap) {
fpp.addFilter(new com.jme3.post.filters.ToneMapFilter());
}
dofFilter = new com.jme3.post.filters.DepthOfFieldFilter();
dofFilter.setFocusDistance(0f);
dofFilter.setFocusRange(0f);
dofFilter.setBlurScale(1.5f);
dofFilter.setEnabled(false);
fpp.addFilter(dofFilter);
if (pfx.fxaa) {
fpp.addFilter(new com.jme3.post.filters.FXAAFilter());
}
app.getViewPort().addProcessor(fpp); app.getViewPort().addProcessor(fpp);
} }

View File

@@ -35,6 +35,7 @@ import de.blight.common.model.trigger.ChangeRoutineTrigger;
import de.blight.common.model.trigger.NpcStatusTrigger; import de.blight.common.model.trigger.NpcStatusTrigger;
import de.blight.common.model.trigger.Trigger; import de.blight.common.model.trigger.Trigger;
import de.blight.game.audio.DialogTtsService; import de.blight.game.audio.DialogTtsService;
import de.blight.game.scene.WorldScene;
import de.blight.game.config.MenuCanvas; import de.blight.game.config.MenuCanvas;
import de.blight.game.config.NinePatch; import de.blight.game.config.NinePatch;
import de.blight.lang.TextResolver; import de.blight.lang.TextResolver;
@@ -224,7 +225,7 @@ public class DialogHudState extends BaseAppState {
int optIdx = scrollOffset + slot; int optIdx = scrollOffset + slot;
if (optIdx >= total) break; if (optIdx >= total) break;
float ty = baseY - slot * LINE_H_OPT; float ty = baseY - slot * LINE_H_OPT;
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) { if (vy >= ty - LINE_H_OPT && vy < ty + FONT_OPT) {
if (selectedOpt != optIdx) { if (selectedOpt != optIdx) {
selectedOpt = optIdx; selectedOpt = optIdx;
renderOptions(); renderOptions();
@@ -320,6 +321,10 @@ public class DialogHudState extends BaseAppState {
this.pageIdx = 0; this.pageIdx = 0;
renderCurrentTextPage(speaker); renderCurrentTextPage(speaker);
if (tts != null) tts.speak(rawText); if (tts != null) tts.speak(rawText);
// Shot-Reverse-Shot: NPC spricht → über Schulter des Spielers; Spieler spricht → über NPC-Schulter
WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class);
if (npcs != null) npcs.switchDialogCamera(textPhase == Phase.TEXT_NPC);
} }
private void renderCurrentTextPage(String speaker) { private void renderCurrentTextPage(String speaker) {
@@ -358,6 +363,10 @@ public class DialogHudState extends BaseAppState {
onOptionsShown.run(); onOptionsShown.run();
} }
// Optionen = Spieler ist am Zug → Kamera über Schulter des NPC (blickt auf Spieler)
WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class);
if (npcs != null) npcs.switchDialogCamera(false);
for (DialogOption opt : options) { for (DialogOption opt : options) {
// Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten) // Primär: erster Hero-Step oder textHero (Fallback für Legacy-Daten)
String label = ""; String label = "";
@@ -480,7 +489,7 @@ public class DialogHudState extends BaseAppState {
// Klick auf Scroll-Pfeil ▲ // Klick auf Scroll-Pfeil ▲
if (scrollOffset > 0) { if (scrollOffset > 0) {
float ty = baseY + LINE_H_OPT; float ty = baseY + LINE_H_OPT;
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) { if (vy >= ty - LINE_H_OPT && vy < ty + FONT_OPT) {
scrollOffset = Math.max(0, scrollOffset - 1); scrollOffset = Math.max(0, scrollOffset - 1);
renderOptions(); renderOptions();
return; return;
@@ -490,7 +499,7 @@ public class DialogHudState extends BaseAppState {
// Klick auf Scroll-Pfeil ▼ // Klick auf Scroll-Pfeil ▼
if (scrollOffset + MAX_OPTIONS < total) { if (scrollOffset + MAX_OPTIONS < total) {
float ty = baseY - MAX_OPTIONS * LINE_H_OPT; float ty = baseY - MAX_OPTIONS * LINE_H_OPT;
if (vy >= ty - LINE_H_OPT + 4f && vy <= ty + 4f) { if (vy >= ty - LINE_H_OPT && vy < ty + FONT_OPT) {
scrollOffset = Math.min(total - MAX_OPTIONS, scrollOffset + 1); scrollOffset = Math.min(total - MAX_OPTIONS, scrollOffset + 1);
renderOptions(); renderOptions();
return; return;
@@ -502,8 +511,8 @@ public class DialogHudState extends BaseAppState {
int optIdx = scrollOffset + slot; int optIdx = scrollOffset + slot;
if (optIdx >= total) break; if (optIdx >= total) break;
float ty = baseY - slot * LINE_H_OPT; float ty = baseY - slot * LINE_H_OPT;
float by = ty - LINE_H_OPT + 4f; float by = ty - LINE_H_OPT;
float byEnd = ty + 4f; float byEnd = ty + FONT_OPT;
if (vy >= by && vy <= byEnd) { if (vy >= by && vy <= byEnd) {
log.debug("[DialogHud] Treffer: Option {} (slot {} ty={})", optIdx, slot, ty); log.debug("[DialogHud] Treffer: Option {} (slot {} ty={})", optIdx, slot, ty);
selectedOpt = optIdx; selectedOpt = optIdx;
@@ -716,6 +725,9 @@ public class DialogHudState extends BaseAppState {
autoAdvanceTimer = -1f; autoAdvanceTimer = -1f;
if (tts != null) tts.stop(); if (tts != null) tts.stop();
setHudsVisible(true); setHudsVisible(true);
WorldScene ws = getStateManager().getState(WorldScene.class);
if (ws != null) ws.clearDof();
unregisterInput(); unregisterInput();
if (canvasNode != null) { if (canvasNode != null) {
canvasNode.setCullHint(Spatial.CullHint.Always); canvasNode.setCullHint(Spatial.CullHint.Always);

View File

@@ -35,6 +35,7 @@ import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.KeyBindings; import de.blight.game.config.KeyBindings;
import de.blight.game.control.PlayerInputControl; import de.blight.game.control.PlayerInputControl;
import de.blight.game.control.ThirdPersonCamera; import de.blight.game.control.ThirdPersonCamera;
import de.blight.game.scene.WorldScene;
import de.blight.game.state.TerrainChunkState; import de.blight.game.state.TerrainChunkState;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -497,40 +498,72 @@ public class WorldNpcsState extends BaseAppState {
} }
private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) { private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) {
// NPC-Spatial auf Bodenniveau; playerPos = Physik-Kapsel-Mitte → auf NPC-Boden projizieren
float groundY = npcPos.y;
Vector3f npcFeet = new Vector3f(npcPos.x, groundY, npcPos.z);
Vector3f playerFeet = new Vector3f(playerPos.x, groundY, playerPos.z);
float charDist = Math.max(1.0f, npcFeet.distance(playerFeet));
// Lookat: Brust-/Schulterhöhe zwischen beiden Figuren
Vector3f lookAt = npcFeet.add(playerFeet).multLocal(0.5f);
lookAt.y += 1.2f;
// Senkrechte Seite zur NPC↔Spieler-Achse
Vector3f axis = npcFeet.subtract(playerFeet);
if (axis.lengthSquared() < 0.001f) { axis.set(1f, 0f, 0f); }
axis.normalizeLocal();
Vector3f side = axis.cross(Vector3f.UNIT_Y).normalizeLocal();
// FOV 45° → tan(22.5°) ≈ 0.41 → für charDist/2 sichtbar: dist = (charDist/2)/0.41 × Puffer
// Faktor 2.5 gibt genug Rand dass Figuren + Umgebung im Bild sind; Minimum 5 m
float dist = Math.max(5.0f, charDist * 2.5f);
float camUp = 1.6f;
// Seite wählen: erhöhter Horizontalstrahl (2 m über lookAt), um Terrain-Clipping zu vermeiden.
// avoidClipping() wird NICHT für die Distanz genutzt, weil Bodenstrahlen bei großen
// Distanzen häufig auf Geländekanten treffen und die Kamera fälschlich nah platzieren.
Vector3f elevated = lookAt.add(0f, 2f, 0f);
float freeA = openDistance(elevated, side);
float freeB = openDistance(elevated, side.negate());
Vector3f chosen = freeA >= freeB ? side : side.negate();
Vector3f camPos = lookAt.add(chosen.mult(dist)).add(0f, camUp, 0f);
if (thirdPersonCam != null) { thirdPersonCam.setPaused(true); } if (thirdPersonCam != null) { thirdPersonCam.setPaused(true); }
// NPC spricht zuerst → Kamera über Schulter des Spielers (Zuhörer)
switchDialogCamera(true);
}
/**
* Kamera für Shot-Reverse-Shot umschalten.
* npcSpeaking=true → Kamera hinter Spieler, blickt auf NPC
* npcSpeaking=false → Kamera hinter NPC, blickt auf Spieler
*/
public void switchDialogCamera(boolean npcSpeaking) {
if (dialogNpc == null || physicsChar == null) return;
// physicsChar.getPhysicsLocation() = Kapsel-Mitte (≈ 0,9 m über Boden)
Vector3f playerCenter = physicsChar.getPhysicsLocation();
Vector3f npcFeet = dialogNpc.visual().getLocalTranslation();
float playerFaceY = playerCenter.y + 0.9f;
float npcFaceY = npcFeet.y + 1.6f;
// Kamera-Abstand über dem Gesicht aus dem NPC-Fall ableiten, damit beide Shots identisch wirken
float camAboveFace = playerFaceY - npcFaceY; // Spielerschulter minus NPC-Gesicht (≈ 0.2 m)
if (npcSpeaking) {
// Listener = Spieler, Speaker = NPC
placeCamera(
playerCenter.x, playerFaceY, playerCenter.z,
npcFeet.x, npcFaceY, npcFeet.z
);
} else {
// Listener = NPC, Speaker = Spieler — gleicher Winkel wie im NPC-Fall
placeCamera(
npcFeet.x, playerFaceY + camAboveFace, npcFeet.z,
playerCenter.x, playerFaceY, playerCenter.z
);
}
}
private void placeCamera(float lisX, float lisY, float lisZ,
float spkX, float spkY, float spkZ) {
Vector3f shoulder = new Vector3f(lisX, lisY, lisZ);
Vector3f speakerFace = new Vector3f(spkX, spkY, spkZ);
// Horizontale Richtung Listener → Sprecher
Vector3f toSpeaker = new Vector3f(spkX - lisX, 0f, spkZ - lisZ);
if (toSpeaker.lengthSquared() < 0.001f) { toSpeaker.set(0f, 0f, 1f); }
toSpeaker.normalizeLocal();
// Schulterseite: die Seite mit mehr Freiraum wählen
Vector3f rightA = toSpeaker.cross(Vector3f.UNIT_Y).normalizeLocal();
Vector3f elevated = shoulder.add(0f, 0.5f, 0f);
Vector3f right = openDistance(elevated, rightA) >= openDistance(elevated, rightA.negate())
? rightA : rightA.negate();
// Kamera: hinter Listener, leicht zur Seite versetzt, weiter zurück für steileren Winkel
Vector3f camPos = shoulder
.add(right.mult(0.45f))
.subtract(toSpeaker.mult(1.5f));
// Gesicht nahe oberem Bildrand: Kamera deutlich unterhalb des Gesichts anzielen
Vector3f lookTarget = new Vector3f(spkX, spkY - 0.9f, spkZ);
cam.setLocation(camPos); cam.setLocation(camPos);
cam.lookAt(lookAt, Vector3f.UNIT_Y); cam.lookAt(lookTarget, Vector3f.UNIT_Y);
// DOF-Fokuspunkt = Gesicht des Sprechers (dynamischer Abstand)
float focusDist = camPos.distance(speakerFace);
WorldScene ws = getStateManager().getState(WorldScene.class);
if (ws != null) { ws.focusDof(focusDist, 10f, 0.8f); }
} }
/** Freier Abstand in {@code dir} ab {@code origin} bis zur ersten Kollision (max Float.MAX_VALUE). */ /** Freier Abstand in {@code dir} ab {@code origin} bis zur ersten Kollision (max Float.MAX_VALUE). */
@@ -597,7 +630,12 @@ public class WorldNpcsState extends BaseAppState {
}); });
} }
// ── Accessor für InteractionHudState ────────────────────────────────────── // ── Accessoren ────────────────────────────────────────────────────────────
/** Liefert das Spatial des aktuellen Dialog-NPCs, oder null wenn kein Dialog aktiv. */
public com.jme3.scene.Spatial getDialogNpcSpatial() {
return dialogNpc != null ? dialogNpc.visual() : null;
}
public String getHoveredLabelKey() { public String getHoveredLabelKey() {
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null; if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;

View File

@@ -58,6 +58,7 @@ menu.graphics.row.shadow=Schattenqualität
menu.graphics.val.shadow.low=Niedrig menu.graphics.val.shadow.low=Niedrig
menu.graphics.val.shadow.medium=Mittel menu.graphics.val.shadow.medium=Mittel
menu.graphics.val.shadow.high=Hoch menu.graphics.val.shadow.high=Hoch
menu.graphics.row.postfx=Post-Effekte
menu.graphics.val.resolution.native=Nativ menu.graphics.val.resolution.native=Nativ
menu.graphics.btn.apply=Übernehmen menu.graphics.btn.apply=Übernehmen
menu.graphics.btn.cancel=Abbrechen menu.graphics.btn.cancel=Abbrechen

View File

@@ -61,6 +61,7 @@ menu.graphics.row.shadow=Shadow Quality
menu.graphics.val.shadow.low=Low menu.graphics.val.shadow.low=Low
menu.graphics.val.shadow.medium=Medium menu.graphics.val.shadow.medium=Medium
menu.graphics.val.shadow.high=High menu.graphics.val.shadow.high=High
menu.graphics.row.postfx=Post Effects
menu.graphics.val.resolution.native=Native menu.graphics.val.resolution.native=Native
menu.graphics.btn.apply=Apply menu.graphics.btn.apply=Apply
menu.graphics.btn.cancel=Cancel menu.graphics.btn.cancel=Cancel