Voxel Klippen System weiter optimiert, Monolog System implementiert, Compiler Warnungen behoben
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user