Voxel Klippen System weiter optimiert, Monolog System implementiert, Compiler Warnungen behoben

This commit is contained in:
2026-07-23 20:29:51 +02:00
parent 9ebde59d40
commit 5a7d9897e4
65 changed files with 2365 additions and 179 deletions

View File

@@ -330,7 +330,10 @@ public class WorldScene extends BaseAppState {
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
app.getStateManager().attach(
new WorldInteractableState(keyBindings, physicsChar, playerInput));
app.getStateManager().attach(new DialogHudState());
de.blight.common.MonologueRegistry.init(de.blight.common.MonologueIO.load());
de.blight.game.state.DialogHudState dialogHud = new de.blight.game.state.DialogHudState();
dialogHud.setMainCharacter(mc);
app.getStateManager().attach(dialogHud);
WorldNpcsState npcsState = new WorldNpcsState(keyBindings, physicsChar, playerInput, mc);
npcsState.setThirdPersonCamera(thirdPersonCam);
app.getStateManager().attach(npcsState);

View File

@@ -34,10 +34,14 @@ import de.blight.common.model.TextReference;
import de.blight.common.model.trigger.ChangeRoutineTrigger;
import de.blight.common.model.trigger.NpcStatusTrigger;
import de.blight.common.model.trigger.Trigger;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import de.blight.common.model.Monologue;
import de.blight.game.audio.DialogTtsService;
import de.blight.game.scene.WorldScene;
import de.blight.game.config.MenuCanvas;
import de.blight.game.config.NinePatch;
import de.blight.lang.AudioResolver;
import de.blight.lang.TextResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -168,9 +172,16 @@ public class DialogHudState extends BaseAppState {
private BitmapText scrollUpText;
private BitmapText scrollDownText;
// ── TTS ───────────────────────────────────────────────────────────────────
// ── Audio ─────────────────────────────────────────────────────────────────
private DialogTtsService tts;
private AudioNode dialogAudioNode;
// ── Monologue polling ────────────────────────────────────────────────────
private MainCharacter mainCharForMonologue;
/** Gesetzt während eines Monologs: kein HUD-Hide, kein Cursor, freie Bewegung. */
private boolean monologueMode = false;
// ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -207,6 +218,12 @@ public class DialogHudState extends BaseAppState {
advanceText();
}
}
if (phase == Phase.HIDDEN && mainCharForMonologue != null) {
Monologue pending = mainCharForMonologue.pollPendingMonologue();
if (pending != null) {
startMonologue(pending, mainCharForMonologue);
}
}
}
private void updateHover() {
@@ -242,6 +259,7 @@ public class DialogHudState extends BaseAppState {
@Override
protected void cleanup(Application app) {
stopDialogAudio();
if (tts != null) {
tts.shutdown();
tts = null;
@@ -284,7 +302,7 @@ public class DialogHudState extends BaseAppState {
TextReference greeting = npc.getDefaultMessage();
String greetText = greeting != null ? TextResolver.get().resolve(greeting) : null;
if (greetText != null && !greetText.isBlank()) {
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, this::closeDialog);
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, null, this::closeDialog);
} else {
log.info("[DialogHud] NPC '{}' hat keine Optionen und keine Begrüßung Dialog übersprungen.", npc.getCharacterId());
closeDialog();
@@ -296,6 +314,36 @@ public class DialogHudState extends BaseAppState {
return phase != Phase.HIDDEN;
}
/** Registriert den Hauptcharakter für Monolog-Polling in update(). */
public void setMainCharacter(MainCharacter mc) {
this.mainCharForMonologue = mc;
}
/**
* Startet ein Selbstgespräch des Hauptcharakters (kein NPC beteiligt).
* HUD bleibt sichtbar, Cursor bleibt verborgen, Bewegung ist weiterhin möglich.
* Audio wird abgespielt (Datei wenn vorhanden, sonst TTS).
* Quest-Folgen werden nach dem letzten Schritt angewendet.
*/
public void startMonologue(Monologue monologue, MainCharacter mc) {
this.currentNpc = null;
this.mainChar = mc;
this.onClose = null;
this.onOptionsShown = null;
this.optionsShownFired = false;
this.monologueMode = true;
// HUD bleibt sichtbar, Cursor bleibt verborgen
canvasNode.setCullHint(Spatial.CullHint.Inherit);
registerMonologueInput();
String speaker = TextResolver.get().resolveId("dialog.speaker.player");
playSteps(Phase.TEXT_HERO, speaker, monologue.getHeroSteps(), 0, () -> {
mc.handleMonologue(monologue);
closeDialog();
});
}
// ── Text-Anzeige ─────────────────────────────────────────────────────────
private Runnable afterText;
@@ -311,22 +359,47 @@ public class DialogHudState extends BaseAppState {
playSteps(ph, speaker, steps, idx + 1, after);
return;
}
showText(ph, speaker, text, () -> playSteps(ph, speaker, steps, idx + 1, after));
String audioPath = step.getAudio() != null ? AudioResolver.get().resolve(step.getAudio()) : null;
showText(ph, speaker, text, audioPath, () -> playSteps(ph, speaker, steps, idx + 1, after));
}
private void showText(Phase textPhase, String speaker, String rawText, Runnable after) {
private void showText(Phase textPhase, String speaker, String rawText, String audioPath, Runnable after) {
this.phase = textPhase;
this.afterText = after;
this.textPages = paginate(wrapText(rawText, MAX_CHARS_LINE), MAX_TEXT_LINES);
this.pageIdx = 0;
renderCurrentTextPage(speaker);
if (tts != null) tts.speak(rawText);
stopDialogAudio();
if (audioPath != null && !audioPath.isBlank()) {
try {
dialogAudioNode = new AudioNode(app.getAssetManager(), audioPath, AudioData.DataType.Buffer);
dialogAudioNode.setPositional(false);
dialogAudioNode.setLooping(false);
app.getRootNode().attachChild(dialogAudioNode);
dialogAudioNode.play();
} catch (Exception e) {
log.warn("[DialogHud] Audio konnte nicht abgespielt werden: {}", audioPath);
dialogAudioNode = null;
if (tts != null) tts.speak(rawText);
}
} else {
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 stopDialogAudio() {
if (dialogAudioNode != null) {
dialogAudioNode.stop();
dialogAudioNode.removeFromParent();
dialogAudioNode = null;
}
}
private void renderCurrentTextPage(String speaker) {
nameText.setText(speaker);
nameText.setCullHint(Spatial.CullHint.Inherit);
@@ -464,6 +537,7 @@ public class DialogHudState extends BaseAppState {
if (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO) {
autoAdvanceTimer = -1f;
if (tts != null) tts.stop();
stopDialogAudio();
pageIdx = textPages.size();
if (afterText != null) { Runnable cb = afterText; afterText = null; cb.run(); }
}
@@ -724,10 +798,15 @@ public class DialogHudState extends BaseAppState {
phase = Phase.HIDDEN;
autoAdvanceTimer = -1f;
if (tts != null) tts.stop();
setHudsVisible(true);
stopDialogAudio();
if (!monologueMode) {
setHudsVisible(true);
WorldScene ws = getStateManager().getState(WorldScene.class);
if (ws != null) ws.clearDof();
}
monologueMode = false;
WorldScene ws = getStateManager().getState(WorldScene.class);
if (ws != null) ws.clearDof();
unregisterInput();
if (canvasNode != null) {
canvasNode.setCullHint(Spatial.CullHint.Always);
@@ -754,6 +833,15 @@ public class DialogHudState extends BaseAppState {
im.setCursorVisible(true);
}
/** Monolog-Variante: nur Enter zum Weiterschalten, kein Cursor, keine LMB-Übernahme. */
private void registerMonologueInput() {
var im = app.getInputManager();
im.addMapping(ACT_CONFIRM, new KeyTrigger(KeyInput.KEY_RETURN),
new KeyTrigger(KeyInput.KEY_NUMPADENTER));
im.addListener(inputListener, ACT_CONFIRM);
// Cursor und Maus-Listener bleiben unberührt Spieler behält volle Bewegung
}
private void unregisterInput() {
var im = app.getInputManager();
try { im.removeListener(inputListener); } catch (Exception ignored) {}

View File

@@ -309,7 +309,8 @@ public class RiverState extends BaseAppState {
}
buf.flip();
Texture2D tex = new Texture2D(new Image(Image.Format.RGBA8, size, size, buf));
Texture2D tex = new Texture2D(new Image(Image.Format.RGBA8, size, size, buf, null,
com.jme3.texture.image.ColorSpace.Linear));
tex.setWrap(Texture.WrapMode.Repeat);
tex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
tex.setMagFilter(Texture.MagFilter.Bilinear);