diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/feuerstelle1.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/feuerstelle1.j3o.thumb.png index 49fd27b..3ad1cc6 100644 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/imported/feuerstelle1.j3o.thumb.png and b/blight-assets/src/main/resources/.thumbnails/Models/imported/feuerstelle1.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o b/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o index b135eee..30b181f 100644 Binary files a/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o and b/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o differ diff --git a/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o.meta b/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o.meta index 1a2701b..c541252 100644 --- a/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o.meta +++ b/blight-assets/src/main/resources/Models/imported/feuerstelle1.j3o.meta @@ -1,6 +1,10 @@ -#Tue Aug 25 19:02:59 CEST 2026 -attachedEmitters.count=0 -attachedLights.count=0 +#Mon Sep 14 11:35:19 CEST 2026 +attachedEmitter.0=0.00000|0.00000|0.00000|Effects/Explosion/flame.png|2|2|1.0000|0.7000|0.1000|1.0000|1.0000|0.1000|0.0000|0.0000|0.2739|0.6573|0.0000|0.7500|0.0000|0.5000|0.0000|-0.1000|0.0000|1.0000|3.0000|60|25.0000 +attachedEmitters.count=1 +attachedLight.0=0.00000|0.70000|0.00000|1.00000|0.60000|0.40000|2.00000|8.00000 +attachedLights.count=1 +attachedSound.0=0.00000|0.50000|0.00000|audio/ambient/fire/campfire.ogg|1.0000|20.0000 +attachedSounds.count=1 castShadow=true category= cullDistance=120.0 @@ -14,6 +18,7 @@ lod1Distance=30.0 lod1Path= lod2Distance=80.0 lod2Path= +modelCategory=NONE name=feuerstelle1 pivotOffsetY=0.0 placementOffsetY=0.0 diff --git a/blight-assets/src/main/resources/audio/ambient/fire/campfire.ogg b/blight-assets/src/main/resources/audio/ambient/fire/campfire.ogg new file mode 100644 index 0000000..fb5642a Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/fire/campfire.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/fire/torch.ogg b/blight-assets/src/main/resources/audio/ambient/fire/torch.ogg new file mode 100644 index 0000000..8282c7b Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/fire/torch.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/water/waterfall.ogg b/blight-assets/src/main/resources/audio/ambient/water/waterfall.ogg index 2416d79..6f53c5e 100644 Binary files a/blight-assets/src/main/resources/audio/ambient/water/waterfall.ogg and b/blight-assets/src/main/resources/audio/ambient/water/waterfall.ogg differ diff --git a/blight-common/src/main/java/de/blight/common/ModelMeta.java b/blight-common/src/main/java/de/blight/common/ModelMeta.java index 64ed684..e6249fe 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMeta.java +++ b/blight-common/src/main/java/de/blight/common/ModelMeta.java @@ -28,6 +28,7 @@ public record ModelMeta( float cullDistance, List attachedLights, List attachedEmitters, + List attachedSounds, de.blight.common.model.InteractableType interactableType, float interactableOffsetX, float interactableOffsetY, @@ -42,17 +43,51 @@ public record ModelMeta( float r, float g, float b, float intensity, float radius) {} - /** Partikel-Emitter relativ zum Modell-Ursprung. Preset: 0=Feuer, 1=Rauch, 2=Funken. */ + /** Sound-Quelle relativ zum Modell-Ursprung. */ + public record AttachedSound( + float offsetX, float offsetY, float offsetZ, + String soundPath, + float volume, + float radius) {} + + /** Partikel-Emitter relativ zum Modell-Ursprung — vollständige Parameter wie PlacedEmitter. */ public record AttachedEmitter( float offsetX, float offsetY, float offsetZ, - int preset) {} + String texturePath, int imagesX, int imagesY, + float startR, float startG, float startB, float startA, + float endR, float endG, float endB, float endA, + float startSize, float endSize, + float velX, float velY, float velZ, float velocityVariation, + float gravX, float gravY, float gravZ, + float lowLife, float highLife, + int maxParticles, float emitRate) { + + public static AttachedEmitter fire(float ox, float oy, float oz) { + return new AttachedEmitter(ox, oy, oz, + "Effects/Explosion/flame.png", 2, 2, + 1f, 0.7f, 0.1f, 1f, 1f, 0.1f, 0f, 0f, + 0.5f, 1.2f, 0f, 2.5f, 0f, 0.5f, 0f, -0.1f, 0f, 1f, 3f, 60, 25f); + } + public static AttachedEmitter smoke(float ox, float oy, float oz) { + return new AttachedEmitter(ox, oy, oz, + "Effects/Smoke/Smoke.png", 1, 1, + 0.5f, 0.5f, 0.5f, 0.4f, 0.2f, 0.2f, 0.2f, 0f, + 0.8f, 2.5f, 0f, 1.2f, 0f, 0.3f, 0f, 0.05f, 0f, 3f, 6f, 24, 6f); + } + public static AttachedEmitter sparks(float ox, float oy, float oz) { + return new AttachedEmitter(ox, oy, oz, + "Effects/Explosion/spark.png", 1, 1, + 1f, 0.9f, 0.2f, 1f, 1f, 0.3f, 0f, 0f, + 0.05f, 0.02f, 0f, 4f, 0f, 1.0f, 0f, -5f, 0f, 0.5f, 2f, 50, 20f); + } + } public static ModelMeta defaults(String j3oFileName) { String name = j3oFileName.replaceFirst("\\.j3o$", ""); return new ModelMeta(name, "", "", 1f, 1f, 1f, true, 0f, 0f, false, true, true, 1f, 1f, "", "", 30f, 80f, 120f, - List.of(), List.of(), + List.of(), List.of(), List.of(), de.blight.common.model.InteractableType.NONE, 0f, 0.5f, 0f, 0f, "", de.blight.common.ModelCategory.NONE); diff --git a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java index 652d35c..41e0094 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java +++ b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java @@ -59,8 +59,27 @@ public final class ModelMetaIO { for (int i = 0; i < emitters.size(); i++) { ModelMeta.AttachedEmitter e = emitters.get(i); p.setProperty("attachedEmitter." + i, String.format(Locale.ROOT, - "%.5f|%.5f|%.5f|%d", - e.offsetX(), e.offsetY(), e.offsetZ(), e.preset())); + "%.5f|%.5f|%.5f|%s|%d|%d|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%.4f|%d|%.4f", + e.offsetX(), e.offsetY(), e.offsetZ(), + e.texturePath(), e.imagesX(), e.imagesY(), + e.startR(), e.startG(), e.startB(), e.startA(), + e.endR(), e.endG(), e.endB(), e.endA(), + e.startSize(), e.endSize(), + e.velX(), e.velY(), e.velZ(), e.velocityVariation(), + e.gravX(), e.gravY(), e.gravZ(), + e.lowLife(), e.highLife(), + e.maxParticles(), e.emitRate())); + } + + // Anhänge: Sounds + List sounds = m.attachedSounds(); + p.setProperty("attachedSounds.count", String.valueOf(sounds.size())); + for (int i = 0; i < sounds.size(); i++) { + ModelMeta.AttachedSound s = sounds.get(i); + p.setProperty("attachedSound." + i, String.format(Locale.ROOT, + "%.5f|%.5f|%.5f|%s|%.4f|%.4f", + s.offsetX(), s.offsetY(), s.offsetZ(), + s.soundPath(), s.volume(), s.radius())); } try (Writer w = Files.newBufferedWriter(metaPath(j3oPath))) { @@ -103,11 +122,48 @@ public final class ModelMetaIO { String v = p.getProperty("attachedEmitter." + i, ""); if (!v.isEmpty()) { String[] f = v.split("\\|", -1); - if (f.length >= 4) { - try { + try { + if (f.length >= 27) { + // Neues Format mit vollständigen Parametern emitters.add(new ModelMeta.AttachedEmitter( Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]), - Integer.parseInt(f[3]))); + f[3], Integer.parseInt(f[4]), Integer.parseInt(f[5]), + Float.parseFloat(f[6]), Float.parseFloat(f[7]), Float.parseFloat(f[8]), Float.parseFloat(f[9]), + Float.parseFloat(f[10]), Float.parseFloat(f[11]), Float.parseFloat(f[12]), Float.parseFloat(f[13]), + Float.parseFloat(f[14]), Float.parseFloat(f[15]), + Float.parseFloat(f[16]), Float.parseFloat(f[17]), Float.parseFloat(f[18]), Float.parseFloat(f[19]), + Float.parseFloat(f[20]), Float.parseFloat(f[21]), Float.parseFloat(f[22]), + Float.parseFloat(f[23]), Float.parseFloat(f[24]), + Integer.parseInt(f[25]), Float.parseFloat(f[26]))); + } else if (f.length >= 4) { + // Altes Format (offsetX|offsetY|offsetZ|preset) → Preset-Defaults übernehmen + float ox = Float.parseFloat(f[0]); + float oy = Float.parseFloat(f[1]); + float oz = Float.parseFloat(f[2]); + int preset = Integer.parseInt(f[3]); + emitters.add(switch (preset) { + case 1 -> ModelMeta.AttachedEmitter.smoke(ox, oy, oz); + case 2 -> ModelMeta.AttachedEmitter.sparks(ox, oy, oz); + default -> ModelMeta.AttachedEmitter.fire(ox, oy, oz); + }); + } + } catch (NumberFormatException ignored) {} + } + } + + // Anhänge: Sounds + int soundCount = parseInt(p, "attachedSounds.count", 0); + List sounds = new ArrayList<>(soundCount); + for (int i = 0; i < soundCount; i++) { + String v = p.getProperty("attachedSound." + i, ""); + if (!v.isEmpty()) { + String[] f = v.split("\\|", -1); + if (f.length >= 6) { + try { + sounds.add(new ModelMeta.AttachedSound( + Float.parseFloat(f[0]), Float.parseFloat(f[1]), Float.parseFloat(f[2]), + f[3], + Float.parseFloat(f[4]), Float.parseFloat(f[5]))); } catch (NumberFormatException ignored) {} } } @@ -135,6 +191,7 @@ public final class ModelMetaIO { parseFloat(p, "cullDistance", 120f), Collections.unmodifiableList(lights), Collections.unmodifiableList(emitters), + Collections.unmodifiableList(sounds), de.blight.common.model.InteractableType.fromString( p.getProperty("interactableType", "NONE")), parseFloat(p, "interactableOffsetX", 0f), diff --git a/blight-common/src/main/java/de/blight/common/PlacedSound.java b/blight-common/src/main/java/de/blight/common/PlacedSound.java new file mode 100644 index 0000000..ac58032 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/PlacedSound.java @@ -0,0 +1,8 @@ +package de.blight.common; + +public record PlacedSound( + float x, float y, float z, + String soundPath, + float volume, + float radius +) {} diff --git a/blight-common/src/main/java/de/blight/common/SoundIO.java b/blight-common/src/main/java/de/blight/common/SoundIO.java new file mode 100644 index 0000000..2482d5e --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/SoundIO.java @@ -0,0 +1,58 @@ +package de.blight.common; + +import java.io.*; +import java.nio.file.*; +import java.util.*; + +/** + * Liest und schreibt platzierte Sound-Quellen als tab-separierte Textdatei + * ({@code blight_sounds.bls}) neben der Kartendatei. + * + * Spalten: x y z soundPath volume radius + */ +public final class SoundIO { + + private SoundIO() {} + + public static Path getPath() { + return MapIO.getMapPath().resolveSibling("blight_sounds.bls"); + } + + public static void save(List sounds) throws IOException { + Path p = getPath(); + Files.createDirectories(p.getParent()); + try (BufferedWriter w = Files.newBufferedWriter(p)) { + w.write("# x\ty\tz\tsoundPath\tvolume\tradius"); + w.newLine(); + for (PlacedSound s : sounds) { + w.write(String.format(Locale.ROOT, + "%.5f\t%.5f\t%.5f\t%s\t%.4f\t%.4f%n", + s.x(), s.y(), s.z(), + s.soundPath(), + s.volume(), s.radius())); + } + } + } + + public static List load() throws IOException { + Path p = getPath(); + if (!Files.exists(p)) return List.of(); + List list = new ArrayList<>(); + for (String line : Files.readAllLines(p)) { + line = line.strip(); + if (line.isEmpty() || line.startsWith("#")) continue; + String[] f = line.split("\t", -1); + if (f.length < 6) continue; + try { + list.add(new PlacedSound( + Float.parseFloat(f[0]), + Float.parseFloat(f[1]), + Float.parseFloat(f[2]), + f[3], + Float.parseFloat(f[4]), + Float.parseFloat(f[5]))); + } catch (NumberFormatException ignored) {} + } + return list; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 57c6acd..9bca28f 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -165,7 +165,8 @@ public class EditorApp extends Application { this::setStatus, this::setCenterView, root::setRight, node -> topBar.getChildren().set(1, node), () -> worldViewport, ASSET_ROOT, this::switchToWorldEditor, registry, - root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool + root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool, + this::scheduleAutoSave ); vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx); ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx); @@ -447,6 +448,14 @@ public class EditorApp extends Application { atmosphereController.updateEmitterPanel(input.selectedEmitterInfo); } + if (input.auswahlSoundSelected) { + input.auswahlSoundSelected = false; + topBarController.getAtmosphaereBtn().setSelected(true); + input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS; + root.setRight(atmosphereController.buildPanel()); + atmosphereController.updateSoundPanel(input.selectedSoundInfo); + } + if (input.auswahlWaterSelected) { input.auswahlWaterSelected = false; topBarController.getWasserMergedBtn().setSelected(true); @@ -548,6 +557,9 @@ public class EditorApp extends Application { setStatus("Auto-Speicherung…"); } }); + + input.onObjectDeleted = () -> + Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); } // ── Modus-Wechsel ──────────────────────────────────────────────────────── @@ -930,6 +942,14 @@ public class EditorApp extends Application { if (input.activeLayer == SharedInput.LAYER_OBJECTS || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT) input.deleteSelectedRequested = true; + else if (input.activeLayer == SharedInput.LAYER_LIGHTS && input.selectedLightInfo != null) + input.deleteLightRequested = true; + else if (input.activeLayer == SharedInput.LAYER_EMITTERS && input.selectedEmitterInfo != null) + input.deleteEmitterRequested = true; + else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null) + input.deleteSoundRequested = true; + else if (input.activeLayer == SharedInput.LAYER_ITEMS && input.selectedItemInfo != null) + input.deleteSelectedItemRequested = true; else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) { if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); } else { input.deleteSoundAreaRequested = true; scheduleAutoSave(); } @@ -965,8 +985,10 @@ public class EditorApp extends Application { confirm.setHeaderText(null); confirm.initOwner(primaryStage); confirm.showAndWait().ifPresent(btn -> { - if (btn == javafx.scene.control.ButtonType.YES) + if (btn == javafx.scene.control.ButtonType.YES) { input.deleteWaterfallRequested = true; + topBarController.getAuswahlBtn().fire(); + } }); }); } diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index 9b73a79..619304e 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -26,6 +26,7 @@ import de.blight.editor.state.LocationZoneState; import de.blight.editor.state.RiverEditorState; import de.blight.editor.state.WaterfallEditorState; import de.blight.editor.state.PlayToolState; +import de.blight.editor.state.PlacedSoundState; import de.blight.editor.state.SoundAreaState; import de.blight.editor.state.WaterBodyState; import de.blight.editor.state.EzTreeState; @@ -204,6 +205,7 @@ public class JmeEditorApp extends SimpleApplication { stateManager.attach(new WillowGeneratorState(input)); stateManager.attach(new LightState(input)); stateManager.attach(new EmitterState(input)); + stateManager.attach(new PlacedSoundState(input)); stateManager.attach(new WaterBodyState(input)); stateManager.attach(new SoundAreaState(input)); stateManager.attach(new AreaState(input)); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 850b3a6..6eb4734 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -445,6 +445,10 @@ public class SharedInput { /** activeLayer==8 → Partikel-Emitter platzieren und bearbeiten */ public static final int LAYER_EMITTERS = 8; + // ── Platzierte Sound-Quellen ────────────────────────────────────────────── + /** activeLayer==35 → einzelne Sound-Quellen platzieren und bearbeiten */ + public static final int LAYER_PLACED_SOUNDS = 35; + /** Klick im Viewport im Emitter-Modus: platzieren oder selektieren. */ public record EmitterClick(float screenX, float screenY, boolean rightButton) {} public final ConcurrentLinkedQueue emitterClickQueue = new ConcurrentLinkedQueue<>(); @@ -469,6 +473,35 @@ public class SharedInput { /** JavaFX → JME: Preset für nächsten neu platzierten Emitter (0=Feuer,1=Rauch,2=Funken). */ public volatile int emitterPreset = 0; + // ── Sound-Quellen-Werkzeug ──────────────────────────────────────────────── + /** Klick im Viewport im Sound-Modus: platzieren oder selektieren. */ + public record SoundClick(float screenX, float screenY, boolean rightButton) {} + public final ConcurrentLinkedQueue soundClickQueue = new ConcurrentLinkedQueue<>(); + + /** JME → JavaFX: Info der selektierten Sound-Quelle. Format: "idx|x|y|z|soundPath|volume|radius" oder null. */ + public volatile String selectedSoundInfo = null; + public volatile boolean soundSelectionChanged = false; + + /** JavaFX → JME: aktualisierte Parameter der selektierten Sound-Quelle. */ + public record SoundPropertyChange(String soundPath, float volume, float radius) {} + public final AtomicReference pendingSoundProp = new AtomicReference<>(); + + /** JavaFX → JME: Selektierte Sound-Quelle löschen. */ + public volatile boolean deleteSoundRequested = false; + + /** JME3-intern: Auswahl-Klick für PlacedSoundState. */ + public final ConcurrentLinkedQueue auswahlSoundClickQueue = new ConcurrentLinkedQueue<>(); + /** JME3 → JavaFX: Sound-Quelle im Auswahl-Modus selektiert → Atmosphäre/Sound-Werkzeug aktivieren. */ + public volatile boolean auswahlSoundSelected = false; + + // ── Kaskaden-Löschung (SceneObject → Lichter/Emitter/Sounds) ───────────── + /** SceneObjectState → LightState: Weltpositionen von zu löschenden Lichtern. */ + public final ConcurrentLinkedQueue removeLightPositions = new ConcurrentLinkedQueue<>(); + /** SceneObjectState → EmitterState: Weltpositionen von zu löschenden Emittern. */ + public final ConcurrentLinkedQueue removeEmitterPositions = new ConcurrentLinkedQueue<>(); + /** SceneObjectState → PlacedSoundState: Weltpositionen von zu löschenden Sounds. */ + public final ConcurrentLinkedQueue removeSoundPositions = new ConcurrentLinkedQueue<>(); + // ── Wasser-Werkzeug ─────────────────────────────────────────────────────── /** activeLayer==9 → Wasseroberflächen platzieren und bearbeiten */ public static final int LAYER_WATER = 9; @@ -794,6 +827,7 @@ public class SharedInput { /** JFX → JME: aktualisierte Anhang-Listen; JME rendert Gizmos neu. */ public volatile java.util.List modelEditorAttachedLights = java.util.List.of(); public volatile java.util.List modelEditorAttachedEmitters = java.util.List.of(); + public volatile java.util.List modelEditorAttachedSounds = java.util.List.of(); public volatile boolean modelEditorAttachmentsChanged = false; /** JFX → JME: Pfad des zu ladenden Modells (relativ zu blight-assets/src/main/resources/). */ @@ -1223,6 +1257,7 @@ public class SharedInput { public volatile boolean auswahlWaterSelected = false; /** JME3 → JavaFX: Wasserfall im Auswahl-Modus selektiert → Wasserfall-Werkzeug aktivieren. */ public volatile boolean auswahlWaterfallSelected = false; + // auswahlSoundSelected + auswahlSoundClickQueue → defined in Sound-Quellen-Werkzeug section above // ── Voxel-Textur-Malen ──────────────────────────────────────────────────── /** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt, @@ -1241,4 +1276,11 @@ public class SharedInput { public final ConcurrentLinkedQueue sceneObjectPathRenames = new ConcurrentLinkedQueue<>(); /** EmitterState pollt diese Queue und ersetzt betroffene PlacedEmitter-Records. */ public final ConcurrentLinkedQueue emitterPathRenames = new ConcurrentLinkedQueue<>(); + + /** + * JME3 → JavaFX: Callback nach Löschung eines ganzen Objekts (Zone, Knoten, Licht, …). + * States rufen dies nach der Löschung auf; EditorApp setzt den Callback auf den FX-Thread. + * Nie bei Vertex-Löschung aufrufen. + */ + public Runnable onObjectDeleted = () -> {}; } diff --git a/blight-editor/src/main/java/de/blight/editor/ToolContext.java b/blight-editor/src/main/java/de/blight/editor/ToolContext.java index af256d6..a2cd7ad 100644 --- a/blight-editor/src/main/java/de/blight/editor/ToolContext.java +++ b/blight-editor/src/main/java/de/blight/editor/ToolContext.java @@ -27,6 +27,7 @@ import java.util.function.Supplier; * @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane * @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper * @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing) + * @param scheduleAutoSave Triggert den 8-Sekunden-Autosave-Timer neu (FX-Thread) */ public record ToolContext( SharedInput input, @@ -43,5 +44,6 @@ public record ToolContext( Consumer setLeftPanel, Consumer setBottomPanel, Supplier centerStackSupplier, - Consumer setCurrentTool + Consumer setCurrentTool, + Runnable scheduleAutoSave ) {} diff --git a/blight-editor/src/main/java/de/blight/editor/state/AreaState.java b/blight-editor/src/main/java/de/blight/editor/state/AreaState.java index 117d28f..357e7e6 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AreaState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AreaState.java @@ -128,7 +128,7 @@ public class AreaState extends BaseAppState { if (input.deleteAreaRequested) { input.deleteAreaRequested = false; - if (selectedIdx >= 0) removeArea(selectedIdx); + if (selectedIdx >= 0) { removeArea(selectedIdx); input.onObjectDeleted.run(); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java b/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java index 40db071..169ce88 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java @@ -47,7 +47,8 @@ public class EmitterState extends BaseAppState { private final List markers = new ArrayList<>(); private final List particles = new ArrayList<>(); - private int selectedIdx = -1; + private int selectedIdx = -1; + private int lastKnownLayer = -1; private List pendingEmitters = null; public EmitterState(SharedInput input) { @@ -80,8 +81,23 @@ public class EmitterState extends BaseAppState { // ── Update ──────────────────────────────────────────────────────────────── + private static final float POS_EPSILON = 0.05f; + @Override public void update(float tpf) { + float[] rmPos; + while ((rmPos = input.removeEmitterPositions.poll()) != null) { + removeByPosition(rmPos[0], rmPos[1], rmPos[2]); + } + + // Beim Verlassen des Emitter-Layers Selektion aufheben + if (lastKnownLayer == SharedInput.LAYER_EMITTERS + && input.activeLayer != SharedInput.LAYER_EMITTERS + && selectedIdx >= 0) { + deselect(); + } + lastKnownLayer = input.activeLayer; + SharedInput.PathRename rename; while ((rename = input.emitterPathRenames.poll()) != null) { for (int i = 0; i < emitters.size(); i++) { @@ -121,7 +137,11 @@ public class EmitterState extends BaseAppState { if (input.deleteEmitterRequested) { input.deleteEmitterRequested = false; - if (selectedIdx >= 0) removeEmitter(selectedIdx); + if (selectedIdx >= 0) { + removeEmitter(selectedIdx); + try { de.blight.common.EmitterIO.save(new ArrayList<>(emitters)); } catch (java.io.IOException ignored) {} + input.onObjectDeleted.run(); + } } } @@ -188,6 +208,8 @@ public class EmitterState extends BaseAppState { deselect(); selectedIdx = idx; setMarkerColor(idx, SEL_COLOR); + setMarkerVisible(idx, false); + setParticleVisible(idx, true); publishSelection(idx); } @@ -195,12 +217,25 @@ public class EmitterState extends BaseAppState { if (selectedIdx >= 0 && selectedIdx < emitters.size()) { PlacedEmitter e = emitters.get(selectedIdx); setMarkerColor(selectedIdx, new ColorRGBA(e.startR(), e.startG(), e.startB(), 1f)); + setMarkerVisible(selectedIdx, true); + setParticleVisible(selectedIdx, false); } selectedIdx = -1; input.selectedEmitterInfo = null; input.emitterSelectionChanged = true; } + private void setMarkerVisible(int idx, boolean visible) { + markers.get(idx).setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + + private void setParticleVisible(int idx, boolean visible) { + ParticleEmitter pe = particles.get(idx); + if (pe != null) { + pe.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + } + private void publishSelection(int idx) { PlacedEmitter e = emitters.get(idx); input.selectedEmitterInfo = buildInfoString(idx, e); @@ -234,7 +269,10 @@ public class EmitterState extends BaseAppState { markers.add(marker); ParticleEmitter particle = buildParticleEmitter(pe); - if (particle != null) rootNode.attachChild(particle); + if (particle != null) { + particle.setCullHint(Spatial.CullHint.Always); // standardmäßig unsichtbar + rootNode.attachChild(particle); + } particles.add(particle); emitters.add(pe); @@ -252,6 +290,25 @@ public class EmitterState extends BaseAppState { input.emitterSelectionChanged = true; } + private void removeByPosition(float x, float y, float z) { + for (int i = emitters.size() - 1; i >= 0; i--) { + PlacedEmitter e = emitters.get(i); + if (Math.abs(e.x() - x) < POS_EPSILON + && Math.abs(e.y() - y) < POS_EPSILON + && Math.abs(e.z() - z) < POS_EPSILON) { + if (selectedIdx == i) { + selectedIdx = -1; + input.selectedEmitterInfo = null; + input.emitterSelectionChanged = true; + } else if (selectedIdx > i) { + selectedIdx--; + } + removeEmitter(i); + break; + } + } + } + private void clearAll() { for (Node m : markers) rootNode.detachChild(m); for (ParticleEmitter pe : particles) if (pe != null) rootNode.detachChild(pe); @@ -270,15 +327,18 @@ public class EmitterState extends BaseAppState { Node newMarker = buildMarker(updated); setMarkerColor(newMarker, SEL_COLOR); + newMarker.setCullHint(Spatial.CullHint.Always); // selektiert → Marker versteckt rootNode.attachChild(newMarker); markers.set(idx, newMarker); ParticleEmitter newPe = buildParticleEmitter(updated); - if (newPe != null) rootNode.attachChild(newPe); + if (newPe != null) rootNode.attachChild(newPe); // selektiert → Partikel sichtbar particles.set(idx, newPe); emitters.set(idx, updated); - publishSelection(idx); + // selectedEmitterInfo aktualisieren, aber kein Panel-Rebuild auslösen – + // sonst werden initStartSize/initVelY neu erfasst und Groesse-Buttons compoundieren. + input.selectedEmitterInfo = buildInfoString(idx, updated); } // ── Marker visuals ──────────────────────────────────────────────────────── diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java index 0f6b614..7d22c16 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -737,7 +737,7 @@ public class EzTreeState extends BaseAppState { true, true, 1f, 1f, "", "", 30f, 80f, 120f, - java.util.List.of(), java.util.List.of(), + java.util.List.of(), java.util.List.of(), java.util.List.of(), de.blight.common.model.InteractableType.NONE, 0f, 0.5f, 0f, 0f, "bark", autoCategory); de.blight.common.ModelMetaIO.save(meta, outPath); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java index f039e6d..b03b2c2 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java @@ -152,6 +152,7 @@ public class ItemPlacementState extends BaseAppState { if (selectedItemIdx >= 0) { removeItem(selectedItemIdx); deselectItem(); + input.onObjectDeleted.run(); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/LightState.java b/blight-editor/src/main/java/de/blight/editor/state/LightState.java index 9b7b534..cd1ce89 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/LightState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/LightState.java @@ -84,8 +84,15 @@ public class LightState extends BaseAppState { // ── Update ──────────────────────────────────────────────────────────────── + private static final float POS_EPSILON = 0.05f; + @Override public void update(float tpf) { + float[] rmPos; + while ((rmPos = input.removeLightPositions.poll()) != null) { + removeByPosition(rmPos[0], rmPos[1], rmPos[2]); + } + SharedInput.ObjectClick auswahlClick; while ((auswahlClick = input.auswahlLightClickQueue.poll()) != null) { handleAuswahlClick(auswahlClick); @@ -105,7 +112,11 @@ public class LightState extends BaseAppState { if (input.deleteLightRequested) { input.deleteLightRequested = false; - if (selectedIdx >= 0) removeLight(selectedIdx); + if (selectedIdx >= 0) { + removeLight(selectedIdx); + try { de.blight.common.LightIO.save(new ArrayList<>(lights)); } catch (java.io.IOException ignored) {} + input.onObjectDeleted.run(); + } } } @@ -221,6 +232,25 @@ public class LightState extends BaseAppState { input.lightSelectionChanged = true; } + private void removeByPosition(float x, float y, float z) { + for (int i = lights.size() - 1; i >= 0; i--) { + PlacedLight l = lights.get(i); + if (Math.abs(l.x() - x) < POS_EPSILON + && Math.abs(l.y() - y) < POS_EPSILON + && Math.abs(l.z() - z) < POS_EPSILON) { + if (selectedIdx == i) { + selectedIdx = -1; + input.selectedLightInfo = null; + input.lightSelectionChanged = true; + } else if (selectedIdx > i) { + selectedIdx--; + } + removeLight(i); + break; + } + } + } + private void clearAll() { for (PointLight pl : pointLights) rootNode.removeLight(pl); for (Node m : markers) rootNode.detachChild(m); diff --git a/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java b/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java index 807b0e5..9831f82 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/LocationZoneState.java @@ -127,7 +127,7 @@ public class LocationZoneState extends BaseAppState { if (input.deleteLocationZoneRequested) { input.deleteLocationZoneRequested = false; - if (selectedIdx >= 0) removeZone(selectedIdx); + if (selectedIdx >= 0) { removeZone(selectedIdx); input.onObjectDeleted.run(); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java index 25bb40b..543e1cf 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java @@ -28,6 +28,9 @@ import com.jme3.scene.shape.Cylinder; import com.jme3.scene.shape.Dome; import com.jme3.scene.shape.Sphere; import com.jme3.anim.SkinningControl; +import com.jme3.effect.ParticleEmitter; +import com.jme3.effect.ParticleMesh; +import com.jme3.material.RenderState; import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; import de.blight.editor.util.ImpostorUtil; @@ -333,7 +336,8 @@ public class ModelEditorState extends BaseAppState { input.modelEditorAttachmentsChanged = false; rebuildAttachmentGizmos( input.modelEditorAttachedLights, - input.modelEditorAttachedEmitters); + input.modelEditorAttachedEmitters, + input.modelEditorAttachedSounds); } // Thumbnail auf Anforderung generieren (Model-Editor) @@ -953,7 +957,8 @@ public class ModelEditorState extends BaseAppState { private void rebuildAttachmentGizmos( java.util.List lights, - java.util.List emitters) { + java.util.List emitters, + java.util.List sounds) { if (previewRoot == null) return; @@ -965,7 +970,7 @@ public class ModelEditorState extends BaseAppState { attachmentGizmos = null; } - if (lights.isEmpty() && emitters.isEmpty()) return; + if (lights.isEmpty() && emitters.isEmpty() && sounds.isEmpty()) return; attachmentGizmos = new Node("attachment_gizmos"); @@ -991,21 +996,22 @@ public class ModelEditorState extends BaseAppState { previewRoot.addLight(pl); } - // Emitter-Gizmos: kleine Kugel in Preset-Farbe + // Emitter: echte ParticleEmitter, keine Kugel-Gizmos for (de.blight.common.ModelMeta.AttachedEmitter ae : emitters) { - ColorRGBA color = switch (ae.preset()) { - case 1 -> new ColorRGBA(0.5f, 0.5f, 0.5f, 1f); // Rauch - case 2 -> new ColorRGBA(1.0f, 0.9f, 0.1f, 1f); // Funken - default -> new ColorRGBA(1.0f, 0.4f, 0.0f, 1f); // Feuer - }; - Geometry sphere = new Geometry("emitter_gizmo", - new Sphere(8, 8, 0.10f)); - Material mat = new Material(app.getAssetManager(), + ParticleEmitter pe = buildAttachedParticleEffect(ae); + if (pe != null) attachmentGizmos.attachChild(pe); + } + + // Sound-Gizmos: kleine türkise Kugel + for (de.blight.common.ModelMeta.AttachedSound as : sounds) { + Geometry soundSphere = new Geometry("sound_gizmo", + new com.jme3.scene.shape.Sphere(8, 8, 0.10f)); + Material sMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color); - sphere.setMaterial(mat); - sphere.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ()); - attachmentGizmos.attachChild(sphere); + sMat.setColor("Color", new ColorRGBA(0.0f, 0.85f, 0.85f, 1f)); + soundSphere.setMaterial(sMat); + soundSphere.setLocalTranslation(as.offsetX(), as.offsetY(), as.offsetZ()); + attachmentGizmos.attachChild(soundSphere); } previewRoot.attachChild(attachmentGizmos); @@ -1019,6 +1025,34 @@ public class ModelEditorState extends BaseAppState { } } + private ParticleEmitter buildAttachedParticleEffect(de.blight.common.ModelMeta.AttachedEmitter ae) { + try { + ParticleEmitter effect = new ParticleEmitter( + "attached_particle", ParticleMesh.Type.Triangle, ae.maxParticles()); + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + mat.setTexture("Texture", app.getAssetManager().loadTexture(ae.texturePath())); + effect.setMaterial(mat); + effect.setImagesX(ae.imagesX()); + effect.setImagesY(ae.imagesY()); + effect.setStartColor(new ColorRGBA(ae.startR(), ae.startG(), ae.startB(), ae.startA())); + effect.setEndColor( new ColorRGBA(ae.endR(), ae.endG(), ae.endB(), ae.endA())); + effect.setStartSize(ae.startSize()); + effect.setEndSize(ae.endSize()); + effect.getParticleInfluencer() + .setInitialVelocity(new Vector3f(ae.velX(), ae.velY(), ae.velZ())); + effect.getParticleInfluencer().setVelocityVariation(ae.velocityVariation()); + effect.setGravity(ae.gravX(), ae.gravY(), ae.gravZ()); + effect.setLowLife(ae.lowLife()); + effect.setHighLife(ae.highLife()); + effect.setParticlesPerSec(ae.emitRate()); + effect.setLocalTranslation(ae.offsetX(), ae.offsetY(), ae.offsetZ()); + return effect; + } catch (Exception e) { + log.warn("[ModelEditorState] Emitter-Textur nicht ladbar: {}", e.getMessage()); + return null; + } + } + // ── Hilfsmethoden ───────────────────────────────────────────────────────── /** Markiert alle Vertex-Buffer als upload-needed – zwingt JME3 zu korrektem erstem Frame. */ diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java index e2047e8..2ecf575 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java @@ -424,7 +424,7 @@ public class ModelImportState extends BaseAppState { def.randomScaleMin(), def.randomScaleMax(), def.lod1Path(), def.lod2Path(), def.lod1Distance(), def.lod2Distance(), def.cullDistance(), - def.attachedLights(), def.attachedEmitters(), + def.attachedLights(), def.attachedEmitters(), def.attachedSounds(), def.interactableType(), def.interactableOffsetX(), def.interactableOffsetY(), def.interactableOffsetZ(), def.interactableRotY(), diff --git a/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java index 11f7b47..e76e3b8 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java @@ -138,7 +138,7 @@ public class PathNetworkEditorState extends BaseAppState { if (input.deletePathNodeRequested) { input.deletePathNodeRequested = false; - deleteSelected(); + if (selectedNode != null) { deleteSelected(); input.onObjectDeleted.run(); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java new file mode 100644 index 0000000..0bf2f43 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java @@ -0,0 +1,312 @@ +package de.blight.editor.state; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.collision.CollisionResults; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.*; +import com.jme3.renderer.Camera; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.*; +import com.jme3.scene.shape.Sphere; +import com.jme3.terrain.geomipmap.TerrainQuad; +import de.blight.common.PlacedSound; +import de.blight.editor.SharedInput; + +import java.util.ArrayList; +import java.util.List; + +public class PlacedSoundState extends BaseAppState { + + private static final float MARKER_RADIUS = 0.30f; + private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f; + private static final float GLOW_ALPHA = 0.18f; + + private static final ColorRGBA COL_NORMAL = new ColorRGBA(0.0f, 0.85f, 0.85f, 1f); + private static final ColorRGBA COL_SELECTED = new ColorRGBA(1.0f, 1.0f, 0.0f, 1f); + private static final ColorRGBA COL_GLOW = new ColorRGBA(0.0f, 0.85f, 0.85f, GLOW_ALPHA); + + private static final float POS_EPSILON = 0.05f; + + private final SharedInput input; + private SimpleApplication app; + private Camera cam; + private AssetManager assets; + private Node rootNode; + private TerrainQuad terrain; + + // parallel lists: sounds[i] <-> markers[i] + private final List sounds = new ArrayList<>(); + private final List markers = new ArrayList<>(); + + private int selectedIdx = -1; + private List pendingSounds = null; + + public PlacedSoundState(SharedInput input) { + this.input = input; + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + protected void initialize(Application application) { + app = (SimpleApplication) application; + cam = app.getCamera(); + assets = app.getAssetManager(); + rootNode = app.getRootNode(); + } + + @Override + protected void cleanup(Application application) { + clearAll(); + } + + @Override + protected void onEnable() { + if (pendingSounds != null) { + loadPlacedSounds(pendingSounds); + pendingSounds = null; + } + } + + @Override protected void onDisable() {} + + public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } + + // ── Update ──────────────────────────────────────────────────────────────── + + @Override + public void update(float tpf) { + float[] rmPos; + while ((rmPos = input.removeSoundPositions.poll()) != null) { + removeByPosition(rmPos[0], rmPos[1], rmPos[2]); + } + + SharedInput.ObjectClick auswahlClick; + while ((auswahlClick = input.auswahlSoundClickQueue.poll()) != null) { + handleAuswahlClick(auswahlClick); + } + + if (input.activeLayer != SharedInput.LAYER_PLACED_SOUNDS) return; + + SharedInput.SoundClick click; + while ((click = input.soundClickQueue.poll()) != null) { + handleClick(click); + } + + SharedInput.SoundPropertyChange prop = input.pendingSoundProp.getAndSet(null); + if (prop != null && selectedIdx >= 0) { + applyProperty(selectedIdx, prop); + } + + if (input.deleteSoundRequested) { + input.deleteSoundRequested = false; + if (selectedIdx >= 0) { + removeSound(selectedIdx); + try { de.blight.common.SoundIO.save(new ArrayList<>(sounds)); } catch (java.io.IOException ignored) {} + input.onObjectDeleted.run(); + } + } + } + + // ── Click handling ──────────────────────────────────────────────────────── + + private void handleClick(SharedInput.SoundClick click) { + float jmeX = click.screenX() * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + + Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); + Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); + Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); + + int hit = pickMarker(ray); + if (hit >= 0) { + if (click.rightButton()) { + deselect(); + } else { + selectSound(hit); + } + return; + } + + if (click.rightButton()) { + deselect(); + return; + } + + Vector3f pt = raycastTerrain(ray); + if (pt == null) return; + + PlacedSound ps = new PlacedSound(pt.x, pt.y + 0.5f, pt.z, "", 1.0f, 20.0f); + addSound(ps); + selectSound(sounds.size() - 1); + } + + private void handleAuswahlClick(SharedInput.ObjectClick click) { + float jmeX = click.screenX() * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); + Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); + Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); + int hit = pickMarker(ray); + if (hit >= 0) { + selectSound(hit); + input.auswahlSoundSelected = true; + } + } + + private int pickMarker(Ray ray) { + for (int i = 0; i < markers.size(); i++) { + CollisionResults res = new CollisionResults(); + markers.get(i).collideWith(ray, res); + if (res.size() > 0) return i; + } + return -1; + } + + // ── Selection ───────────────────────────────────────────────────────────── + + private void selectSound(int idx) { + deselect(); + selectedIdx = idx; + setMarkerColor(idx, COL_SELECTED); + publishSelection(idx); + } + + private void deselect() { + if (selectedIdx >= 0 && selectedIdx < sounds.size()) { + setMarkerColor(selectedIdx, COL_NORMAL); + } + selectedIdx = -1; + input.selectedSoundInfo = null; + input.soundSelectionChanged = true; + } + + private void publishSelection(int idx) { + PlacedSound s = sounds.get(idx); + input.selectedSoundInfo = String.format( + java.util.Locale.ROOT, + "%d|%.3f|%.3f|%.3f|%s|%.3f|%.3f", + idx, s.x(), s.y(), s.z(), s.soundPath(), s.volume(), s.radius()); + input.soundSelectionChanged = true; + } + + // ── Add / Remove ────────────────────────────────────────────────────────── + + private void addSound(PlacedSound ps) { + Node marker = buildMarker(ps); + rootNode.attachChild(marker); + sounds.add(ps); + markers.add(marker); + } + + private void removeSound(int idx) { + rootNode.detachChild(markers.get(idx)); + sounds.remove(idx); + markers.remove(idx); + selectedIdx = -1; + input.selectedSoundInfo = null; + input.soundSelectionChanged = true; + } + + private void removeByPosition(float x, float y, float z) { + for (int i = sounds.size() - 1; i >= 0; i--) { + PlacedSound s = sounds.get(i); + if (Math.abs(s.x() - x) < POS_EPSILON + && Math.abs(s.y() - y) < POS_EPSILON + && Math.abs(s.z() - z) < POS_EPSILON) { + if (selectedIdx == i) { + selectedIdx = -1; + input.selectedSoundInfo = null; + input.soundSelectionChanged = true; + } else if (selectedIdx > i) { + selectedIdx--; + } + rootNode.detachChild(markers.get(i)); + sounds.remove(i); + markers.remove(i); + break; + } + } + } + + private void clearAll() { + for (Node m : markers) rootNode.detachChild(m); + sounds.clear(); + markers.clear(); + selectedIdx = -1; + } + + // ── Property application ────────────────────────────────────────────────── + + private void applyProperty(int idx, SharedInput.SoundPropertyChange prop) { + PlacedSound old = sounds.get(idx); + PlacedSound updated = new PlacedSound( + old.x(), old.y(), old.z(), + prop.soundPath(), prop.volume(), prop.radius()); + sounds.set(idx, updated); + publishSelection(idx); + } + + // ── Marker visuals ──────────────────────────────────────────────────────── + + private Node buildMarker(PlacedSound ps) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", COL_NORMAL); + Geometry sphere = new Geometry("sound_sphere", new Sphere(10, 10, MARKER_RADIUS)); + sphere.setMaterial(mat); + + Material glowMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + glowMat.setColor("Color", COL_GLOW); + glowMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + glowMat.getAdditionalRenderState().setDepthWrite(false); + glowMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Front); + Geometry glow = new Geometry("sound_glow", new Sphere(8, 8, GLOW_RADIUS)); + glow.setMaterial(glowMat); + glow.setQueueBucket(RenderQueue.Bucket.Transparent); + + Node node = new Node("sound_node"); + node.attachChild(glow); + node.attachChild(sphere); + node.setLocalTranslation(ps.x(), ps.y(), ps.z()); + return node; + } + + private void setMarkerColor(int idx, ColorRGBA color) { + Node marker = markers.get(idx); + for (Spatial child : marker.getChildren()) { + if (child instanceof Geometry geo && "sound_sphere".equals(geo.getName())) { + geo.getMaterial().setColor("Color", color); + return; + } + } + } + + // ── Save / Load ─────────────────────────────────────────────────────────── + + public List getPlacedSounds() { + return new ArrayList<>(sounds); + } + + public void loadPlacedSounds(List loaded) { + if (rootNode == null) { + pendingSounds = new ArrayList<>(loaded); + return; + } + clearAll(); + for (PlacedSound ps : loaded) { + addSound(ps); + } + } + + private Vector3f raycastTerrain(Ray ray) { + if (terrain == null) return null; + CollisionResults hits = new CollisionResults(); + terrain.collideWith(ray, hits); + if (hits.size() > 0) return hits.getClosestCollision().getContactPoint(); + return null; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index cae8962..22eb173 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -85,9 +85,13 @@ public class SceneObjectState extends BaseAppState { private Node rootNode; private TerrainQuad terrain; - private final List objects = new ArrayList<>(); - private final List objNodes = new ArrayList<>(); - private final List animClips = new ArrayList<>(); // parallel zu objects + private final List objects = new ArrayList<>(); + private final List objNodes = new ArrayList<>(); + private final List animClips = new ArrayList<>(); // parallel zu objects + // Kaskaden-Löschung: Welt-Positionen der mitplatzierten Lichter/Emitter/Sounds pro Objekt + private final List> placedLightPositions = new ArrayList<>(); + private final List> placedEmitterPositions = new ArrayList<>(); + private final List> placedSoundPositions = new ArrayList<>(); private Node objectRoot; private Node gizmoNode; // Wurzel aller Gizmo-Geometrien (direktes Kind von rootNode) @@ -205,6 +209,9 @@ public class SceneObjectState extends BaseAppState { objects.clear(); objNodes.clear(); animClips.clear(); + placedLightPositions.clear(); + placedEmitterPositions.clear(); + placedSoundPositions.clear(); sceneLitMaterials.clear(); windMaterials.clear(); pbrMaterials.clear(); @@ -231,6 +238,9 @@ public class SceneObjectState extends BaseAppState { so.modelCategory = pm.modelCategory() != null ? pm.modelCategory() : de.blight.common.ModelCategory.NONE; objects.add(so); animClips.add(pm.animClip() != null ? pm.animClip() : ""); + placedLightPositions.add(new ArrayList<>()); + placedEmitterPositions.add(new ArrayList<>()); + placedSoundPositions.add(new ArrayList<>()); // meshFile hat Vorrang: enthält exportierte Geometrie inkl. Material String loadPath = !pm.meshFile().isEmpty() ? pm.meshFile() : pm.modelPath(); @@ -465,7 +475,7 @@ public class SceneObjectState extends BaseAppState { // Löschen if (input.deleteSelectedRequested) { input.deleteSelectedRequested = false; - deleteSelected(); + if (!selectedIndices.isEmpty()) { deleteSelected(); input.onObjectDeleted.run(); } } // Zusammenfassen @@ -731,6 +741,7 @@ public class SceneObjectState extends BaseAppState { input.auswahlClickQueue.offer(click); input.auswahlLightClickQueue.offer(click); input.auswahlEmitterClickQueue.offer(click); + input.auswahlSoundClickQueue.offer(click); input.auswahlWaterClickQueue.offer(click); input.auswahlWaterfallClickQueue.offer(click); return; @@ -879,6 +890,9 @@ public class SceneObjectState extends BaseAppState { so.receiveShadow = defaultReceive; objects.add(so); animClips.add(""); + placedLightPositions.add(new ArrayList<>()); + placedEmitterPositions.add(new ArrayList<>()); + placedSoundPositions.add(new ArrayList<>()); Node node = loadModelNode(modelPath, wx, wy + placementOffY, wz); node.setLocalScale(defaultScale); @@ -891,10 +905,11 @@ public class SceneObjectState extends BaseAppState { input.objectJustPlaced = true; setStatus("Platziert: " + modelPath); - // Anhänge aus ModelMeta: Lichter + Emitter mitplatzieren + // Anhänge aus ModelMeta: Lichter + Emitter + Sounds mitplatzieren if (meta != null) { placeAttachedLights(meta, wx, wy + placementOffY, wz); placeAttachedEmitters(meta, wx, wy + placementOffY, wz); + placeAttachedSounds(meta, wx, wy + placementOffY, wz); } } @@ -904,10 +919,12 @@ public class SceneObjectState extends BaseAppState { try { java.util.List list = new java.util.ArrayList<>(de.blight.common.LightIO.load()); + java.util.List track = placedLightPositions.get(objects.size() - 1); for (de.blight.common.ModelMeta.AttachedLight al : meta.attachedLights()) { - list.add(new de.blight.common.PlacedLight( - wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ(), + float lx = wx + al.offsetX(), ly = wy + al.offsetY(), lz = wz + al.offsetZ(); + list.add(new de.blight.common.PlacedLight(lx, ly, lz, al.r(), al.g(), al.b(), al.intensity(), al.radius())); + track.add(new float[]{lx, ly, lz}); } de.blight.common.LightIO.save(list); input.reloadLightsEmitters = true; @@ -922,16 +939,22 @@ public class SceneObjectState extends BaseAppState { try { java.util.List list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load()); + java.util.List track = placedEmitterPositions.get(objects.size() - 1); for (de.blight.common.ModelMeta.AttachedEmitter ae : meta.attachedEmitters()) { float ax = wx + ae.offsetX(); float ay = wy + ae.offsetY(); float az = wz + ae.offsetZ(); - de.blight.common.PlacedEmitter pe = switch (ae.preset()) { - case 1 -> de.blight.common.PlacedEmitter.smoke(ax, ay, az); - case 2 -> de.blight.common.PlacedEmitter.sparks(ax, ay, az); - default -> de.blight.common.PlacedEmitter.fire(ax, ay, az); - }; - list.add(pe); + list.add(new de.blight.common.PlacedEmitter( + ax, ay, az, 20f, + ae.texturePath(), ae.imagesX(), ae.imagesY(), + ae.startR(), ae.startG(), ae.startB(), ae.startA(), + ae.endR(), ae.endG(), ae.endB(), ae.endA(), + ae.startSize(), ae.endSize(), + ae.velX(), ae.velY(), ae.velZ(), ae.velocityVariation(), + ae.gravX(), ae.gravY(), ae.gravZ(), + ae.lowLife(), ae.highLife(), + ae.maxParticles(), ae.emitRate())); + track.add(new float[]{ax, ay, az}); } de.blight.common.EmitterIO.save(list); input.reloadLightsEmitters = true; @@ -940,6 +963,28 @@ public class SceneObjectState extends BaseAppState { } } + private void placeAttachedSounds(de.blight.common.ModelMeta meta, + float wx, float wy, float wz) { + if (meta.attachedSounds().isEmpty()) return; + try { + java.util.List list = + new java.util.ArrayList<>(de.blight.common.SoundIO.load()); + java.util.List track = placedSoundPositions.get(objects.size() - 1); + for (de.blight.common.ModelMeta.AttachedSound as : meta.attachedSounds()) { + float sx = wx + as.offsetX(); + float sy = wy + as.offsetY(); + float sz = wz + as.offsetZ(); + list.add(new de.blight.common.PlacedSound(sx, sy, sz, + as.soundPath(), as.volume(), as.radius())); + track.add(new float[]{sx, sy, sz}); + } + de.blight.common.SoundIO.save(list); + input.reloadLightsEmitters = true; + } catch (java.io.IOException e) { + log.error("[SceneObject] Anhang-Sound speichern fehlgeschlagen: {}", e.getMessage()); + } + } + private void convertUnshadedToPbr(Spatial spatial) { if (spatial instanceof Geometry geo) { Material mat = geo.getMaterial(); @@ -1898,16 +1943,48 @@ public class SceneObjectState extends BaseAppState { List sortedDesc = new ArrayList<>(selectedIndices); sortedDesc.sort(Comparator.reverseOrder()); + // Positionen aller Kaskaden-Anhänge sammeln (aus Meta – funktioniert auch nach Neustart) + List cascadeLights = new ArrayList<>(); + List cascadeEmitters = new ArrayList<>(); + List cascadeSounds = new ArrayList<>(); + for (int idx : sortedDesc) { SceneObject so = objects.get(idx); + de.blight.common.ModelMeta meta = loadMetaCached(so.modelPath); + if (meta != null) { + float wx = so.getWorldX(), wy = so.getGroundY(), wz = so.getWorldZ(); + for (var al : meta.attachedLights()) { + float[] p = {wx + al.offsetX(), wy + al.offsetY(), wz + al.offsetZ()}; + input.removeLightPositions.offer(p); + cascadeLights.add(p); + } + for (var ae : meta.attachedEmitters()) { + float[] p = {wx + ae.offsetX(), wy + ae.offsetY(), wz + ae.offsetZ()}; + input.removeEmitterPositions.offer(p); + cascadeEmitters.add(p); + } + for (var as : meta.attachedSounds()) { + float[] p = {wx + as.offsetX(), wy + as.offsetY(), wz + as.offsetZ()}; + input.removeSoundPositions.offer(p); + cascadeSounds.add(p); + } + } deleteInteractableFile(so); objectRoot.detachChild(objNodes.get(idx)); cachedGizmoRadii.remove(objNodes.get(idx)); objects.remove(idx); objNodes.remove(idx); animClips.remove(idx); + placedLightPositions.remove(idx); + placedEmitterPositions.remove(idx); + placedSoundPositions.remove(idx); } + // Dateien sofort bereinigen, damit reloadLightsEmitters keine alten Daten zurückbringt + if (!cascadeLights.isEmpty()) cascadeRemoveFromFile(cascadeLights, "lights"); + if (!cascadeEmitters.isEmpty()) cascadeRemoveFromFile(cascadeEmitters, "emitters"); + if (!cascadeSounds.isEmpty()) cascadeRemoveFromFile(cascadeSounds, "sounds"); + selectedIndices.clear(); clearSubSelection(); activeGizmo = -1; @@ -1920,6 +1997,42 @@ public class SceneObjectState extends BaseAppState { setStatus("Objekt(e) gelöscht"); } + private void cascadeRemoveFromFile(List positions, String type) { + try { + switch (type) { + case "lights" -> { + List list = new java.util.ArrayList<>(de.blight.common.LightIO.load()); + for (float[] p : positions) { + list.removeIf(l -> Math.abs(l.x()-p[0]) < 0.05f + && Math.abs(l.y()-p[1]) < 0.05f + && Math.abs(l.z()-p[2]) < 0.05f); + } + de.blight.common.LightIO.save(list); + } + case "emitters" -> { + List list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load()); + for (float[] p : positions) { + list.removeIf(e -> Math.abs(e.x()-p[0]) < 0.05f + && Math.abs(e.y()-p[1]) < 0.05f + && Math.abs(e.z()-p[2]) < 0.05f); + } + de.blight.common.EmitterIO.save(list); + } + case "sounds" -> { + List list = new java.util.ArrayList<>(de.blight.common.SoundIO.load()); + for (float[] p : positions) { + list.removeIf(s -> Math.abs(s.x()-p[0]) < 0.05f + && Math.abs(s.y()-p[1]) < 0.05f + && Math.abs(s.z()-p[2]) < 0.05f); + } + de.blight.common.SoundIO.save(list); + } + } + } catch (java.io.IOException e) { + log.warn("[SceneObject] Kaskade {}-Datei bereinigen fehlgeschlagen: {}", type, e.getMessage()); + } + } + private void deleteInteractableFile(SceneObject so) { if (so.interactableId == null || so.interactableId.isEmpty()) return; try { diff --git a/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java b/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java index 78afe20..ce14ee0 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SoundAreaState.java @@ -131,7 +131,7 @@ public class SoundAreaState extends BaseAppState { if (input.deleteSoundAreaRequested) { input.deleteSoundAreaRequested = false; - if (selectedIdx >= 0) removeArea(selectedIdx); + if (selectedIdx >= 0) { removeArea(selectedIdx); input.onObjectDeleted.run(); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index b174b30..c9401a3 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -47,6 +47,7 @@ import de.blight.common.MapIO; import de.blight.common.PlacedModelIO; import de.blight.editor.SharedInput; import de.blight.editor.state.PathNetworkEditorState; +import de.blight.editor.state.PlacedSoundState; import de.blight.editor.state.RoutineMapState; import de.blight.editor.tool.HeightTool; import org.slf4j.Logger; @@ -105,6 +106,7 @@ public class TerrainEditorState extends BaseAppState { private ItemPlacementState itemPlacementState; private LightState lightState; private EmitterState emitterState; + private PlacedSoundState placedSoundState; private WaterBodyState waterBodyState; private SoundAreaState soundAreaState; private AreaState areaState; @@ -326,6 +328,18 @@ public class TerrainEditorState extends BaseAppState { } } + input.loadingStatus = "Lade Sound-Quellen..."; + placedSoundState = app.getStateManager().getState(PlacedSoundState.class); + if (placedSoundState != null) { + placedSoundState.setTerrain(terrain); + try { + var sounds = de.blight.common.SoundIO.load(); + if (!sounds.isEmpty()) placedSoundState.loadPlacedSounds(sounds); + } catch (IOException e) { + log.error("Sound-Quellen nicht ladbar", e); + } + } + input.loadingStatus = "Lade Wasserflächen..."; waterBodyState = app.getStateManager().getState(WaterBodyState.class); if (waterBodyState != null) { @@ -1303,14 +1317,16 @@ public class TerrainEditorState extends BaseAppState { if (input.reloadLightsEmitters) { input.reloadLightsEmitters = false; - try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} - try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {} + try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} + try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {} + try { if (placedSoundState != null) placedSoundState.loadPlacedSounds(de.blight.common.SoundIO.load()); } catch (Exception ignored) {} } if (input.reloadPlacedOther) { input.reloadPlacedOther = false; - try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} - try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {} + try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} + try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {} + try { if (placedSoundState != null) placedSoundState.loadPlacedSounds(de.blight.common.SoundIO.load()); } catch (Exception ignored) {} try { if (waterBodyState != null) waterBodyState.loadPlacedBodies(de.blight.common.WaterBodyIO.load()); } catch (Exception ignored) {} try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {} try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {} @@ -1558,6 +1574,7 @@ public class TerrainEditorState extends BaseAppState { final List models = sceneObjState != null ? sceneObjState.getPlacedModels() : null; final List lights = lightState != null ? lightState.getPlacedLights() : null; final List emitters = emitterState != null ? emitterState.getPlacedEmitters() : null; + final List placedSounds = placedSoundState != null ? placedSoundState.getPlacedSounds() : null; final List waters = waterBodyState != null ? waterBodyState.getPlacedBodies() : null; final List> rivers = riverEditorState != null ? riverEditorState.getPlacedRivers() : null; final List soundAreas = soundAreaState != null ? soundAreaState.getPlacedAreas() : null; @@ -1573,6 +1590,7 @@ public class TerrainEditorState extends BaseAppState { try { if (models != null) PlacedModelIO.save(models); } catch (IOException e) { log.error("Modelle speichern", e); } try { if (lights != null) LightIO.save(lights); } catch (IOException e) { log.error("Lichter speichern", e); } try { if (emitters != null) EmitterIO.save(emitters); } catch (IOException e) { log.error("Emitter speichern", e); } + try { if (placedSounds != null) de.blight.common.SoundIO.save(placedSounds); } catch (IOException e) { log.error("Sound-Quellen speichern", e); } try { if (waters != null) WaterBodyIO.save(waters); } catch (IOException e) { log.error("Wasser speichern", e); } try { if (rivers != null) de.blight.common.RiverIO.save(rivers); } catch (IOException e) { log.error("Flüsse speichern", e); } try { if (soundAreas != null) SoundAreaIO.save(soundAreas); } catch (IOException e) { log.error("Soundbereiche speichern", e); } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java index ca19238..df2b1e2 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java @@ -489,7 +489,7 @@ public class TreeGeneratorState extends BaseAppState { de.blight.common.ModelMeta autoMeta = new de.blight.common.ModelMeta( fileName, "trees", "", 1f, 1f, 1f, true, 0f, 0f, true, true, true, 1f, 1f, "", "", 30f, 80f, 120f, - java.util.List.of(), java.util.List.of(), + java.util.List.of(), java.util.List.of(), java.util.List.of(), de.blight.common.model.InteractableType.NONE, 0f, 0.5f, 0f, 0f, "bark", autoCategory); de.blight.common.ModelMetaIO.save(autoMeta, out.toPath()); diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelCliffEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelCliffEditorState.java index eb39697..249224a 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelCliffEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelCliffEditorState.java @@ -109,9 +109,7 @@ public class VoxelCliffEditorState extends BaseAppState { if (input.deleteVoxelCliffZoneRequested) { input.deleteVoxelCliffZoneRequested = false; - if (selectedIdx >= 0) { - removeZone(selectedIdx); - } + if (selectedIdx >= 0) { removeZone(selectedIdx); input.onObjectDeleted.run(); } } if (input.generateCliffVoxelsRequested) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java b/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java index 0a8fdda..bd57f94 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java @@ -191,8 +191,10 @@ public class WaterBodyState extends BaseAppState { input.deleteWaterRequested = false; if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) { deleteCornerPoint(selectedIdx, selectedHandle); + input.onObjectDeleted.run(); } else if (selectedIdx >= 0) { removeBody(selectedIdx); + input.onObjectDeleted.run(); } } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java b/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java index 84de3b3..7678225 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/atmosphere/AtmosphereController.java @@ -4,6 +4,8 @@ import de.blight.common.PlacedEmitter; import de.blight.editor.SharedInput; import de.blight.editor.ToolContext; import de.blight.editor.toolset.UiHelper; +import de.blight.editor.ui.SoundChooser; +import javafx.application.Platform; import javafx.geometry.Insets; import javafx.scene.control.*; import javafx.scene.layout.*; @@ -14,6 +16,7 @@ public class AtmosphereController { private VBox lightDynamicContent; private VBox emitterDynamicContent; + private VBox soundDynamicContent; public AtmosphereController(ToolContext ctx) { this.ctx = ctx; @@ -28,6 +31,10 @@ public class AtmosphereController { ctx.input().emitterSelectionChanged = false; updateEmitterPanel(ctx.input().selectedEmitterInfo); } + if (ctx.input().soundSelectionChanged) { + ctx.input().soundSelectionChanged = false; + updateSoundPanel(ctx.input().selectedSoundInfo); + } } public VBox buildPanel() { @@ -41,15 +48,19 @@ public class AtmosphereController { ToggleButton subLichtBtn = new ToggleButton("* Licht"); ToggleButton subEmitterBtn = new ToggleButton("~ Emitter"); + ToggleButton subSoundBtn = new ToggleButton("♪ Sound"); subLichtBtn.setMaxWidth(Double.MAX_VALUE); subEmitterBtn.setMaxWidth(Double.MAX_VALUE); + subSoundBtn.setMaxWidth(Double.MAX_VALUE); ToggleGroup subGroup = new ToggleGroup(); subLichtBtn.setToggleGroup(subGroup); subEmitterBtn.setToggleGroup(subGroup); + subSoundBtn.setToggleGroup(subGroup); subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); - HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn); + HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn, subSoundBtn); HBox.setHgrow(subLichtBtn, Priority.ALWAYS); HBox.setHgrow(subEmitterBtn, Priority.ALWAYS); + HBox.setHgrow(subSoundBtn, Priority.ALWAYS); panel.getChildren().add(subRow); VBox contentBox = new VBox(0); @@ -63,13 +74,21 @@ public class AtmosphereController { ctx.input().activeLayer = SharedInput.LAYER_EMITTERS; contentBox.getChildren().setAll(buildEmitterPanel()); }; + Runnable activateSound = () -> { + ctx.input().activeLayer = SharedInput.LAYER_PLACED_SOUNDS; + contentBox.getChildren().setAll(buildSoundPanel()); + }; subLichtBtn.setOnAction(e -> activateLicht.run()); subEmitterBtn.setOnAction(e -> activateEmitter.run()); + subSoundBtn.setOnAction(e -> activateSound.run()); if (ctx.input().activeLayer == SharedInput.LAYER_EMITTERS) { subEmitterBtn.setSelected(true); activateEmitter.run(); + } else if (ctx.input().activeLayer == SharedInput.LAYER_PLACED_SOUNDS) { + subSoundBtn.setSelected(true); + activateSound.run(); } else { subLichtBtn.setSelected(true); activateLicht.run(); @@ -282,6 +301,7 @@ public class AtmosphereController { vals[17], vals[18], mxPart[0], emit[0]); ctx.input().pendingEmitter.set(pe); + ctx.scheduleAutoSave().run(); }; emitterDynamicContent.getChildren().add(paramLabel("Aktivierungsradius")); @@ -296,14 +316,50 @@ public class AtmosphereController { } emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Partikelgröße")); + final float initStartSize = vals[8], initEndSize = vals[9]; + final float initVelX = vals[10], initVelY = vals[11], initVelZ = vals[12]; + Slider groesseSl = new Slider(0.05, 10.0, 1.0); + groesseSl.setBlockIncrement(0.05); + Label groesseValLbl = new Label("1.00"); + groesseValLbl.setStyle("-fx-font-size: 10;"); + groesseValLbl.setMinWidth(38); + groesseSl.valueProperty().addListener((o, ov, nv) -> { + float f = nv.floatValue(); + float sqrtF = (float) Math.sqrt(f); + groesseValLbl.setText(String.format("%.2f", f)); + vals[8] = Math.max(0.01f, initStartSize * sqrtF); + vals[9] = Math.max(0.01f, initEndSize * sqrtF); + vals[10] = initVelX * f; + vals[11] = initVelY * f; + vals[12] = initVelZ * f; + publish.run(); + }); + Label groesseNameLbl = new Label("Groesse"); + groesseNameLbl.setStyle("-fx-font-size: 10;"); + groesseNameLbl.setMinWidth(120); + HBox groesseRow = new HBox(4, groesseNameLbl, groesseSl, groesseValLbl); + HBox.setHgrow(groesseSl, Priority.ALWAYS); + emitterDynamicContent.getChildren().add(groesseRow); + double[] sizeFactors = {0.1, 0.3, 1.0, 3.0, 8.0}; + String[] sizeLabels = {"XS","S","M","L","XL"}; + HBox sizeRow = new HBox(4); + for (int pi = 0; pi < sizeLabels.length; pi++) { + Button sb = new Button(sizeLabels[pi]); + sb.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(sb, Priority.ALWAYS); + final double factor = sizeFactors[pi]; + sb.setOnAction(ev -> groesseSl.setValue(factor)); + sizeRow.getChildren().add(sb); + } + emitterDynamicContent.getChildren().add(sizeRow); emitterDynamicContent.getChildren().add(buildSlider("Start-Größe", vals, 8, 0.01, 5, 0.05, publish)); emitterDynamicContent.getChildren().add(buildSlider("Ende-Größe", vals, 9, 0.01, 5, 0.05, publish)); emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Geschwindigkeit")); - emitterDynamicContent.getChildren().add(buildSlider("Vel X", vals, 10, -10, 10, 0.1, publish)); - emitterDynamicContent.getChildren().add(buildSlider("Vel Y", vals, 11, 0, 15, 0.1, publish)); - emitterDynamicContent.getChildren().add(buildSlider("Vel Z", vals, 12, -10, 10, 0.1, publish)); - emitterDynamicContent.getChildren().add(buildSlider("Variation", vals, 13, 0, 1, 0.05, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Tempo X", vals, 10, -5, 5, 0.05, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Tempo Y", vals, 11, -5, 5, 0.05, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Tempo Z", vals, 12, -5, 5, 0.05, publish)); + emitterDynamicContent.getChildren().add(buildSlider("Streuung (0=eng)", vals, 13, 0, 1, 0.05, publish)); emitterDynamicContent.getChildren().addAll(new Separator(), paramLabel("Gravitation")); emitterDynamicContent.getChildren().add(buildSlider("Grav Y", vals, 15, -20, 20, 0.5, publish)); @@ -376,4 +432,128 @@ public class AtmosphereController { int dot = name.lastIndexOf('.'); return dot > 0 ? name.substring(0, dot) : name; } + + // ── Sound-Panel ─────────────────────────────────────────────────────────── + + private VBox buildSoundPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + UiHelper.sectionTitle("Sound-Quellen"), + new Separator(), + UiHelper.sectionTitle("Gewählte Quelle"), + new Separator()); + + soundDynamicContent = new VBox(6); + Label noSel = new Label("Keine Quelle ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + soundDynamicContent.getChildren().add(noSel); + inner.getChildren().add(soundDynamicContent); + + inner.getChildren().addAll( + new Separator(), + UiHelper.styledHint("Linksklick → platzieren / auswählen"), + UiHelper.styledHint("Rechtsklick → Auswahl aufheben"), + UiHelper.styledHint("Entf → Quelle löschen")); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + public void updateSoundPanel(String info) { + if (soundDynamicContent == null) return; + soundDynamicContent.getChildren().clear(); + + if (info == null) { + Label noSel = new Label("Keine Quelle ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + soundDynamicContent.getChildren().add(noSel); + return; + } + + String[] parts = info.split("\\|", -1); + // format: idx|x|y|z|soundPath|volume|radius + if (parts.length < 7) return; + try { + float x = Float.parseFloat(parts[1]); + float y = Float.parseFloat(parts[2]); + float z = Float.parseFloat(parts[3]); + String path0 = parts[4]; + float vol0 = Float.parseFloat(parts[5]); + float rad0 = Float.parseFloat(parts[6]); + + Label posLabel = new Label(String.format("X: %.1f Y: %.1f Z: %.1f", x, y, z)); + posLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + soundDynamicContent.getChildren().addAll(posLabel, new Separator()); + + final String[] curPath = {path0}; + final float[] curVol = {vol0}; + final float[] curRad = {rad0}; + + Runnable publish = () -> { + ctx.input().pendingSoundProp.set( + new SharedInput.SoundPropertyChange(curPath[0], curVol[0], curRad[0])); + ctx.scheduleAutoSave().run(); + }; + + // Sound-Datei wählen + Label soundLbl = new Label("Sound:"); + soundLbl.setStyle("-fx-font-size: 11;"); + Button soundBtn = new Button(shortFileName(curPath[0])); + soundBtn.setMaxWidth(Double.MAX_VALUE); + soundBtn.setOnAction(e -> Platform.runLater(() -> { + SoundChooser chooser = new SoundChooser(ctx.assetRoot(), SoundChooser.Mode.ALL); + chooser.showAndWait().ifPresent(p -> { + curPath[0] = p; + soundBtn.setText(shortFileName(p)); + publish.run(); + }); + })); + soundDynamicContent.getChildren().addAll(soundLbl, soundBtn, new Separator()); + + // Lautstärke + Label volLbl = new Label("Lautstärke"); + volLbl.setStyle("-fx-font-size: 11;"); + Slider volSl = new Slider(0.0, 1.0, curVol[0]); + volSl.setBlockIncrement(0.05); + volSl.valueProperty().addListener((o, ov, nv) -> { + curVol[0] = nv.floatValue(); + publish.run(); + }); + soundDynamicContent.getChildren().addAll(volLbl, UiHelper.withField(volSl, "%.2f")); + + // Radius + Label radLbl = new Label("Radius"); + radLbl.setStyle("-fx-font-size: 11;"); + Slider radSl = new Slider(0.5, 200.0, curRad[0]); + radSl.setBlockIncrement(1.0); + radSl.valueProperty().addListener((o, ov, nv) -> { + curRad[0] = nv.floatValue(); + publish.run(); + }); + soundDynamicContent.getChildren().addAll(radLbl, UiHelper.withField(radSl, "%.1f")); + + Button delBtn = new Button("Loeschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> ctx.input().deleteSoundRequested = true); + soundDynamicContent.getChildren().addAll(new Separator(), delBtn); + + } catch (NumberFormatException ignored) {} + } + + private String shortFileName(String path) { + if (path == null || path.isEmpty()) return "(kein Sound)"; + String name = java.nio.file.Paths.get(path).getFileName().toString(); + int dot = name.lastIndexOf('.'); + return dot > 0 ? name.substring(0, dot) : name; + } } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java index 3765944..fc7ed59 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java @@ -58,6 +58,8 @@ public class ModelEditorController implements ToolController { private final java.util.List modelEditorEmitters = new java.util.ArrayList<>(); private VBox modelEditorLightBox = null; private VBox modelEditorEmitterBox = null; + private VBox modelEditorSoundBox = null; + private final java.util.List modelEditorSounds = new java.util.ArrayList<>(); private ComboBox modelEditorCategoryCB = null; private ComboBox modelEditorInteractableCB = null; private Spinner modelEditorInteractableXSpin = null; @@ -555,8 +557,8 @@ public class ModelEditorController implements ToolController { HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn); HBox.setHgrow(modelEditorLod2GenBtn, Priority.ALWAYS); - // Anhänge: Lichter & Partikel - Label attachTitle = new Label("Anhänge: Lichter & Partikel"); + // Anhänge: Lichter, Partikel & Sounds + Label attachTitle = new Label("Anhänge: Lichter, Partikel & Sound"); attachTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); Label lightSectionLbl = new Label("Lichtquellen:"); @@ -591,12 +593,29 @@ public class ModelEditorController implements ToolController { addEmitterBtn.setMaxWidth(Double.MAX_VALUE); addEmitterBtn.setStyle("-fx-background-color:#4a2e2e;"); addEmitterBtn.setOnAction(ev -> { - modelEditorEmitters.add(new de.blight.common.ModelMeta.AttachedEmitter( - 0f, 0.5f, 0f, 0)); + modelEditorEmitters.add(de.blight.common.ModelMeta.AttachedEmitter.fire(0f, 0.5f, 0f)); rebuildEmitterBox(); pushAttachmentsToJme(); }); + Label soundSectionLbl = new Label("Sound-Quellen:"); + soundSectionLbl.setStyle("-fx-text-fill:#aaa;"); + + modelEditorSounds.clear(); + modelEditorSounds.addAll(meta.attachedSounds()); + + modelEditorSoundBox = new VBox(4); + rebuildSoundBox(); + + Button addSoundBtn = new Button("+ Sound-Quelle"); + addSoundBtn.setMaxWidth(Double.MAX_VALUE); + addSoundBtn.setStyle("-fx-background-color:#2e3a4a;"); + addSoundBtn.setOnAction(ev -> { + modelEditorSounds.add(new de.blight.common.ModelMeta.AttachedSound(0f, 0.5f, 0f, "", 1.0f, 20.0f)); + rebuildSoundBox(); + pushAttachmentsToJme(); + }); + pushAttachmentsToJme(); // Modell-Kategorie @@ -811,6 +830,7 @@ public class ModelEditorController implements ToolController { modelEditorLod1Path, modelEditorLod2Path, new java.util.ArrayList<>(modelEditorLights), new java.util.ArrayList<>(modelEditorEmitters), + new java.util.ArrayList<>(modelEditorSounds), modelEditorInteractableCB != null ? modelEditorInteractableCB.getValue() : de.blight.common.model.InteractableType.NONE, @@ -861,6 +881,7 @@ public class ModelEditorController implements ToolController { attachTitle, lightSectionLbl, modelEditorLightBox, addLightBtn, emitterSectionLbl, modelEditorEmitterBox, addEmitterBtn, + soundSectionLbl, modelEditorSoundBox, addSoundBtn, new Separator(), catTitle, modelEditorCategoryCB, new Separator(), @@ -1342,6 +1363,7 @@ public class ModelEditorController implements ToolController { String lod1Path, String lod2Path, java.util.List lights, java.util.List emitters, + java.util.List sounds, de.blight.common.model.InteractableType interactableType, float interactableOffsetX, float interactableOffsetY, float interactableOffsetZ, float interactableRotY, @@ -1351,7 +1373,7 @@ public class ModelEditorController implements ToolController { name, category, tags, 1f, 1f, 1f, uniform, pivotY, placeY, solid, cast, receive, rndMin, rndMax, lod1Path, lod2Path, 30f, 80f, 120f, - lights, emitters, + lights, emitters, sounds, interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE, interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY, footstepSurface != null ? footstepSurface : "", @@ -1460,6 +1482,7 @@ public class ModelEditorController implements ToolController { private void pushAttachmentsToJme() { ctx.input().modelEditorAttachedLights = new java.util.ArrayList<>(modelEditorLights); ctx.input().modelEditorAttachedEmitters = new java.util.ArrayList<>(modelEditorEmitters); + ctx.input().modelEditorAttachedSounds = new java.util.ArrayList<>(modelEditorSounds); ctx.input().modelEditorAttachmentsChanged = true; } @@ -1553,34 +1576,134 @@ public class ModelEditorController implements ToolController { tp.setExpanded(false); tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); - GridPane g = new GridPane(); - g.setHgap(6); g.setVgap(3); - g.setPadding(new Insets(4)); - + // Offset-Spinner Spinner ox = buildOffsetSpinner(ae.offsetX()); Spinner oy = buildOffsetSpinner(ae.offsetY()); Spinner oz = buildOffsetSpinner(ae.offsetZ()); - addLabeledRow(g, 0, "Offset X:", ox); - addLabeledRow(g, 1, "Offset Y:", oy); - addLabeledRow(g, 2, "Offset Z:", oz); + GridPane offsetGrid = new GridPane(); + offsetGrid.setHgap(6); offsetGrid.setVgap(3); + offsetGrid.setPadding(new Insets(4, 4, 0, 4)); + addLabeledRow(offsetGrid, 0, "Offset X:", ox); + addLabeledRow(offsetGrid, 1, "Offset Y:", oy); + addLabeledRow(offsetGrid, 2, "Offset Z:", oz); - ComboBox presetCB = new ComboBox<>(); - presetCB.getItems().addAll("Feuer", "Rauch", "Funken"); - presetCB.getSelectionModel().select(Math.max(0, Math.min(2, ae.preset()))); - presetCB.setMaxWidth(Double.MAX_VALUE); - g.add(new Label("Typ:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); - g.add(presetCB, 1, 3); + // Mutable state for all parameters + float[] vals = { + ae.startR(), ae.startG(), ae.startB(), ae.startA(), // 0-3 + ae.endR(), ae.endG(), ae.endB(), ae.endA(), // 4-7 + ae.startSize(), ae.endSize(), // 8-9 + ae.velX(), ae.velY(), ae.velZ(), ae.velocityVariation(), // 10-13 + ae.gravX(), ae.gravY(), ae.gravZ(), // 14-16 + ae.lowLife(), ae.highLife(), ae.emitRate() // 17-19 + }; + int[] mxPart = {ae.maxParticles()}; Runnable sync = () -> { modelEditorEmitters.set(idx, new de.blight.common.ModelMeta.AttachedEmitter( ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), - presetCB.getSelectionModel().getSelectedIndex())); + ae.texturePath(), ae.imagesX(), ae.imagesY(), + vals[0], vals[1], vals[2], vals[3], + vals[4], vals[5], vals[6], vals[7], + vals[8], vals[9], + vals[10], vals[11], vals[12], vals[13], + vals[14], vals[15], vals[16], + vals[17], vals[18], + mxPart[0], vals[19])); pushAttachmentsToJme(); }; - ox.valueProperty().addListener((o,ov,nv) -> sync.run()); - oy.valueProperty().addListener((o,ov,nv) -> sync.run()); - oz.valueProperty().addListener((o,ov,nv) -> sync.run()); - presetCB.valueProperty().addListener((o,ov,nv) -> sync.run()); + ox.valueProperty().addListener((o, ov, nv) -> sync.run()); + oy.valueProperty().addListener((o, ov, nv) -> sync.run()); + oz.valueProperty().addListener((o, ov, nv) -> sync.run()); + + // Preset-Buttons + Button btnFire = new Button("Feuer"); + Button btnSmoke = new Button("Rauch"); + Button btnSparks = new Button("Funken"); + for (Button b : new Button[]{btnFire, btnSmoke, btnSparks}) { + b.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(b, Priority.ALWAYS); + } + btnFire.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.fire( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue()))); + btnSmoke.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.smoke( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue()))); + btnSparks.setOnAction(ev -> applyEmitterPreset(idx, de.blight.common.ModelMeta.AttachedEmitter.sparks( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue()))); + + VBox content = new VBox(4); + content.setPadding(new Insets(2)); + content.getChildren().add(offsetGrid); + content.getChildren().add(new HBox(4, btnFire, btnSmoke, btnSparks)); + + content.getChildren().addAll(emSep(), emLabel("Startfarbe")); + content.getChildren().add(emSlider("Start R", vals, 0, 0, 1, 0.01, sync)); + content.getChildren().add(emSlider("Start G", vals, 1, 0, 1, 0.01, sync)); + content.getChildren().add(emSlider("Start B", vals, 2, 0, 1, 0.01, sync)); + content.getChildren().add(emSlider("Start A", vals, 3, 0, 1, 0.01, sync)); + content.getChildren().addAll(emSep(), emLabel("Endfarbe")); + content.getChildren().add(emSlider("Ende R", vals, 4, 0, 1, 0.01, sync)); + content.getChildren().add(emSlider("Ende G", vals, 5, 0, 1, 0.01, sync)); + content.getChildren().add(emSlider("Ende B", vals, 6, 0, 1, 0.01, sync)); + content.getChildren().add(emSlider("Ende A", vals, 7, 0, 1, 0.01, sync)); + + content.getChildren().addAll(emSep(), emLabel("Partikelgroesse")); + final float initStartSizeE = vals[8], initEndSizeE = vals[9]; + final float initVelXE = vals[10], initVelYE = vals[11], initVelZE = vals[12]; + Slider groesseSlE = new Slider(0.05, 10.0, 1.0); + groesseSlE.setBlockIncrement(0.05); + Label groesseValE = new Label("1.00"); + groesseValE.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + groesseValE.setMinWidth(38); + groesseSlE.valueProperty().addListener((o, ov, nv) -> { + float f = nv.floatValue(); + float sqrtF = (float) Math.sqrt(f); + groesseValE.setText(String.format("%.2f", f)); + vals[8] = Math.max(0.01f, initStartSizeE * sqrtF); + vals[9] = Math.max(0.01f, initEndSizeE * sqrtF); + vals[10] = initVelXE * f; + vals[11] = initVelYE * f; + vals[12] = initVelZE * f; + sync.run(); + }); + Label groesseNameE = new Label("Groesse"); + groesseNameE.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;"); + groesseNameE.setMinWidth(115); + HBox groesseRowE = new HBox(4, groesseNameE, groesseSlE, groesseValE); + HBox.setHgrow(groesseSlE, Priority.ALWAYS); + groesseRowE.setPadding(new Insets(0, 4, 0, 4)); + content.getChildren().add(groesseRowE); + double[] sizeFactors = {0.1, 0.3, 1.0, 3.0, 8.0}; + String[] sizeLabels = {"XS","S","M","L","XL"}; + HBox sizeRow = new HBox(4); + sizeRow.setPadding(new Insets(0, 4, 0, 4)); + for (int pi = 0; pi < sizeLabels.length; pi++) { + Button sb = new Button(sizeLabels[pi]); + sb.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(sb, Priority.ALWAYS); + final double factor = sizeFactors[pi]; + sb.setOnAction(ev -> groesseSlE.setValue(factor)); + sizeRow.getChildren().add(sb); + } + content.getChildren().add(sizeRow); + content.getChildren().add(emSlider("Start-Groesse", vals, 8, 0.01, 5, 0.05, sync)); + content.getChildren().add(emSlider("Ende-Groesse", vals, 9, 0.01, 5, 0.05, sync)); + + content.getChildren().addAll(emSep(), emLabel("Geschwindigkeit")); + content.getChildren().add(emSlider("Tempo X", vals, 10, -5, 5, 0.05, sync)); + content.getChildren().add(emSlider("Tempo Y", vals, 11, -5, 5, 0.05, sync)); + content.getChildren().add(emSlider("Tempo Z", vals, 12, -5, 5, 0.05, sync)); + content.getChildren().add(emSlider("Streuung (0=eng)", vals, 13, 0, 1, 0.05, sync)); + + content.getChildren().addAll(emSep(), emLabel("Gravitation")); + content.getChildren().add(emSlider("Grav Y", vals, 15, -20, 20, 0.5, sync)); + + content.getChildren().addAll(emSep(), emLabel("Lebensdauer (s)")); + content.getChildren().add(emSlider("Min", vals, 17, 0.1, 10, 0.1, sync)); + content.getChildren().add(emSlider("Max", vals, 18, 0.1, 10, 0.1, sync)); + + content.getChildren().addAll(emSep(), emLabel("Emission")); + content.getChildren().add(emSlider("Max Partikel", mxPart, 0, 1, 500, 5, sync)); + content.getChildren().add(emSlider("Rate/s", vals, 19, 0.5, 100, 0.5, sync)); Button del = new Button("✕ Entfernen"); del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); @@ -1590,13 +1713,68 @@ public class ModelEditorController implements ToolController { rebuildEmitterBox(); pushAttachmentsToJme(); }); + content.getChildren().addAll(emSep(), del); - VBox content = new VBox(4, g, del); - content.setPadding(new Insets(2)); tp.setContent(content); return tp; } + private void applyEmitterPreset(int idx, de.blight.common.ModelMeta.AttachedEmitter preset) { + modelEditorEmitters.set(idx, preset); + rebuildEmitterBox(); + pushAttachmentsToJme(); + } + + private static Separator emSep() { return new Separator(); } + + private static Label emLabel(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-size:11; -fx-font-weight:bold; -fx-text-fill:#ccc;"); + return l; + } + + private static HBox emSlider(String name, float[] arr, int idx, + double min, double max, double step, Runnable sync) { + Label lbl = new Label(name); + lbl.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;"); + lbl.setMinWidth(115); + Slider sl = new Slider(min, max, Math.max(min, Math.min(max, arr[idx]))); + sl.setBlockIncrement(step); + Label val = new Label(String.format("%.2f", arr[idx])); + val.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + val.setMinWidth(38); + sl.valueProperty().addListener((o, ov, nv) -> { + arr[idx] = nv.floatValue(); + val.setText(String.format("%.2f", arr[idx])); + sync.run(); + }); + HBox row = new HBox(4, lbl, sl, val); + HBox.setHgrow(sl, Priority.ALWAYS); + row.setPadding(new Insets(0, 4, 0, 4)); + return row; + } + + private static HBox emSlider(String name, int[] arr, int idx, + double min, double max, double step, Runnable sync) { + Label lbl = new Label(name); + lbl.setStyle("-fx-font-size:10; -fx-text-fill:#ccc;"); + lbl.setMinWidth(115); + Slider sl = new Slider(min, max, Math.max(min, Math.min(max, arr[idx]))); + sl.setBlockIncrement(step); + Label val = new Label(String.format("%.0f", (double) arr[idx])); + val.setStyle("-fx-font-size:10; -fx-text-fill:#aaa;"); + val.setMinWidth(38); + sl.valueProperty().addListener((o, ov, nv) -> { + arr[idx] = nv.intValue(); + val.setText(String.format("%.0f", (double) arr[idx])); + sync.run(); + }); + HBox row = new HBox(4, lbl, sl, val); + HBox.setHgrow(sl, Priority.ALWAYS); + row.setPadding(new Insets(0, 4, 0, 4)); + return row; + } + private void importLodFile(int slot) { FileChooser fc = new FileChooser(); fc.setTitle("LOD " + slot + " – j3o wählen"); @@ -1674,4 +1852,86 @@ public class ModelEditorController implements ToolController { new SharedInput.ModelLodGenRequest(level, method, null, reductionPct)); }); } + + // ── Sound-Anhänge ───────────────────────────────────────────────────────── + + private void rebuildSoundBox() { + if (modelEditorSoundBox == null) return; + modelEditorSoundBox.getChildren().clear(); + for (int i = 0; i < modelEditorSounds.size(); i++) { + final int idx = i; + modelEditorSoundBox.getChildren().add(buildSoundEntry(idx, modelEditorSounds.get(i))); + } + } + + private javafx.scene.Node buildSoundEntry(int idx, + de.blight.common.ModelMeta.AttachedSound as) { + TitledPane tp = new TitledPane(); + tp.setText("Sound " + (idx + 1)); + tp.setExpanded(false); + tp.setStyle("-fx-text-fill:#ddd; -fx-background-color:#333;"); + + GridPane g = new GridPane(); + g.setHgap(6); g.setVgap(3); + g.setPadding(new Insets(4)); + + Spinner ox = buildOffsetSpinner(as.offsetX()); + Spinner oy = buildOffsetSpinner(as.offsetY()); + Spinner oz = buildOffsetSpinner(as.offsetZ()); + addLabeledRow(g, 0, "Offset X:", ox); + addLabeledRow(g, 1, "Offset Y:", oy); + addLabeledRow(g, 2, "Offset Z:", oz); + + final String[] curPath = {as.soundPath()}; + Button soundBtn = new Button(curPath[0].isEmpty() ? "(kein Sound)" : java.nio.file.Paths.get(curPath[0]).getFileName().toString()); + soundBtn.setMaxWidth(Double.MAX_VALUE); + g.add(new Label("Sound:") {{ setStyle("-fx-text-fill:#aaa;"); }}, 0, 3); + g.add(soundBtn, 1, 3); + + Spinner volSpin = new Spinner<>(0.0, 1.0, as.volume(), 0.05); + volSpin.setEditable(true); + addLabeledRow(g, 4, "Lautstärke:", volSpin); + + Spinner radSpin = new Spinner<>(0.5, 200.0, as.radius(), 1.0); + radSpin.setEditable(true); + addLabeledRow(g, 5, "Radius:", radSpin); + + Runnable sync = () -> { + modelEditorSounds.set(idx, new de.blight.common.ModelMeta.AttachedSound( + ox.getValue().floatValue(), oy.getValue().floatValue(), oz.getValue().floatValue(), + curPath[0], + volSpin.getValue().floatValue(), radSpin.getValue().floatValue())); + pushAttachmentsToJme(); + }; + + soundBtn.setOnAction(ev -> { + de.blight.editor.ui.SoundChooser chooser = new de.blight.editor.ui.SoundChooser( + ctx.assetRoot(), de.blight.editor.ui.SoundChooser.Mode.ALL); + chooser.showAndWait().ifPresent(p -> { + curPath[0] = p; + soundBtn.setText(java.nio.file.Paths.get(p).getFileName().toString()); + sync.run(); + }); + }); + + ox.valueProperty().addListener((o, ov, nv) -> sync.run()); + oy.valueProperty().addListener((o, ov, nv) -> sync.run()); + oz.valueProperty().addListener((o, ov, nv) -> sync.run()); + volSpin.valueProperty().addListener((o, ov, nv) -> sync.run()); + radSpin.valueProperty().addListener((o, ov, nv) -> sync.run()); + + Button del = new Button("✕ Entfernen"); + del.setStyle("-fx-background-color:#7a2222; -fx-text-fill:#fff;"); + del.setMaxWidth(Double.MAX_VALUE); + del.setOnAction(ev -> { + modelEditorSounds.remove(idx); + rebuildSoundBox(); + pushAttachmentsToJme(); + }); + + VBox content = new VBox(4, g, del); + content.setPadding(new Insets(2)); + tp.setContent(content); + return tp; + } } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java index e4ed524..37b8fbc 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java @@ -446,6 +446,8 @@ public class ViewportController { input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0)); case SharedInput.LAYER_EMITTERS -> input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_PLACED_SOUNDS -> + input.soundClickQueue.offer(new SharedInput.SoundClick((float) x, (float) y, action < 0)); case SharedInput.LAYER_WATER -> input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0)); case SharedInput.LAYER_SOUND_AREAS -> diff --git a/blight-game/src/main/java/de/blight/game/audio/StereoSource.java b/blight-game/src/main/java/de/blight/game/audio/StereoSource.java new file mode 100644 index 0000000..3a04ef9 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/audio/StereoSource.java @@ -0,0 +1,179 @@ +package de.blight.game.audio; + +import com.jme3.asset.AssetManager; +import com.jme3.audio.AudioBuffer; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioKey; +import com.jme3.audio.AudioNode; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.scene.Node; +import com.jme3.util.BufferUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ShortBuffer; + +/** + * Zentrale Abstraktion für positionalen Stereo-Klang (L/R-Panning). + * + * Strategie (identisch zu OceanSoundState): + * Zwei positionale AudioNodes (L + R) bleiben immer PAN_DIST Meter links/rechts + * der Kamera. Das Lautstärkeverhältnis (constant-power Panning) simuliert die + * wahrgenommene Richtung. Distanz-Fading wird vom Aufrufer übergeben. + * + * Stereo-Dateien werden beim Laden automatisch auf Mono downgemixet (L+R average), + * damit setPositional(true) immer funktioniert – transparent für den Aufrufer. + * + * Typische Verwendung: + *
+ *   StereoSource src = StereoSource.tryLoad(assets, "audio/birds.ogg");
+ *   // pro Frame:
+ *   if (inRange  && !src.isAttached()) src.attachAndPlay(rootNode);
+ *   if (src.isAttached())              src.update(cam, soundWorldPos, playerPos, volume);
+ *   if (!inRange &&  src.isAttached()) src.stop(rootNode);
+ * 
+ */ +public final class StereoSource { + + private static final Logger log = LoggerFactory.getLogger(StereoSource.class); + /** L/R-Quellen PAN_DIST Meter neben der Kamera — muss < refDistance (10 m JME3-Default) sein. */ + public static final float PAN_DIST = 2f; + + private final AudioNode nodeL; + private final AudioNode nodeR; + private boolean attached = false; + + // ── Erzeugung ───────────────────────────────────────────────────────────── + + /** + * Lädt die Audiodatei, downmixed Stereo automatisch zu Mono und erstellt + * ein L/R-Paar. Gibt {@code null} zurück wenn die Datei nicht ladbar ist. + */ + public static StereoSource tryLoad(AssetManager assets, String path) { + try { + AudioNode l = buildPositionalNode(assets, path); + AudioNode r = buildPositionalNode(assets, path); + return new StereoSource(l, r); + } catch (Exception e) { + log.warn("[StereoSource] Nicht ladbar '{}': {}", path, e.getMessage()); + return null; + } + } + + private StereoSource(AudioNode nodeL, AudioNode nodeR) { + this.nodeL = nodeL; + this.nodeR = nodeR; + } + + private static AudioNode buildPositionalNode(AssetManager assets, String path) { + AudioKey key = new AudioKey(path, false, true); // Standard-Key (stream=false, streamCache=true) + AudioData ad = (AudioData) assets.loadAsset(key); + + if (ad instanceof AudioBuffer ab && ab.getChannels() == 2) { + ad = downmixToMono(ab); + log.debug("[StereoSource] '{}' downgemixed Stereo→Mono für Positional-Audio.", path); + } + + AudioNode n = new AudioNode(ad, key); + n.setLooping(true); + n.setVolume(0f); + n.setPositional(true); + return n; + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + public void attachAndPlay(Node rootNode) { + if (attached) return; + rootNode.attachChild(nodeL); + rootNode.attachChild(nodeR); + nodeL.play(); + nodeR.play(); + attached = true; + } + + public void stop(Node rootNode) { + if (!attached) return; + nodeL.stop(); + nodeR.stop(); + if (nodeL.getParent() != null) rootNode.detachChild(nodeL); + if (nodeR.getParent() != null) rootNode.detachChild(nodeR); + attached = false; + } + + // ── Update ──────────────────────────────────────────────────────────────── + + /** + * Aktualisiert Position und Lautstärke der L/R-Nodes. + * + * @param cam aktuelle Kamera + * @param soundPos Weltposition der Sound-Quelle + * @param playerPos Weltposition des Spielers + * @param volume Endlautstärke (distanceFactor * baseVol * ambientVol bereits eingerechnet) + */ + public void update(Camera cam, Vector3f soundPos, Vector3f playerPos, float volume) { + Vector3f camLoc = cam.getLocation(); + Vector3f camLeft = cam.getLeft(); + + // L/R-Quellen PAN_DIST Meter neben der Kamera halten + float lx = camLoc.x + camLeft.x * PAN_DIST; + float lz = camLoc.z + camLeft.z * PAN_DIST; + float rx = camLoc.x - camLeft.x * PAN_DIST; + float rz = camLoc.z - camLeft.z * PAN_DIST; + nodeL.setLocalTranslation(lx, camLoc.y, lz); + nodeR.setLocalTranslation(rx, camLoc.y, rz); + + // Pan = Dot(Richtung Spieler→Quelle, Kamera-Rechts) = −Dot mit camLeft + float dx = soundPos.x - playerPos.x; + float dz = soundPos.z - playerPos.z; + float len = (float) Math.sqrt(dx * dx + dz * dz); + float pan = 0f; + if (len > 0.1f) { + dx /= len; dz /= len; + pan = Math.max(-1f, Math.min(1f, -(dx * camLeft.x + dz * camLeft.z))); + } + + // Constant-power Panning + float panL = (float) Math.sqrt((1f - pan) / 2f); + float panR = (float) Math.sqrt((1f + pan) / 2f); + nodeL.setVolume(volume * panL); + nodeR.setVolume(volume * panR); + } + + public boolean isAttached() { return attached; } + + // ── Stereo → Mono Downmix ──────────────────────────────────────────────── + + private static AudioBuffer downmixToMono(AudioBuffer stereo) { + ByteBuffer src = stereo.getData().duplicate().order(ByteOrder.LITTLE_ENDIAN); + src.rewind(); + int bps = stereo.getBitsPerSample(); + int rate = stereo.getSampleRate(); + + ByteBuffer dst; + if (bps == 16) { + ShortBuffer inS = src.asShortBuffer(); + int monoSamples = inS.limit() / 2; + dst = BufferUtils.createByteBuffer(monoSamples * 2); + ShortBuffer outS = dst.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); + for (int i = 0; i < monoSamples; i++) { + outS.put((short) ((inS.get() + inS.get()) >> 1)); + } + } else { + int monoSamples = src.limit() / 2; + dst = BufferUtils.createByteBuffer(monoSamples); + for (int i = 0; i < monoSamples; i++) { + dst.put((byte) (((src.get() & 0xFF) + (src.get() & 0xFF)) / 2)); + } + } + dst.rewind(); + + AudioBuffer mono = new AudioBuffer(); + mono.setupFormat(1, bps, rate); + mono.updateData(dst); + return mono; + } +} diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 39008d2..8f88d9d 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -105,6 +105,8 @@ public class WorldScene extends BaseAppState { private de.blight.game.state.AmbientSoundSystem ambientSounds; private de.blight.game.state.MusicSystem musicSystem; private de.blight.game.state.EmitterSystem emitterSystem; + private de.blight.game.state.WaterfallState waterfallState; + private de.blight.game.state.PlacedSoundSystem placedSoundSystem; private de.blight.game.audio.FootstepSystem footstepSystem; private de.blight.game.state.DrownState drownState; private WaterInteractionState waterInteractionState; @@ -277,9 +279,12 @@ public class WorldScene extends BaseAppState { BlightGame.status("Lade Welt-Objekte..."); app.getStateManager().attach(new RiverState()); - app.getStateManager().attach(new de.blight.game.state.WaterfallState()); + waterfallState = new de.blight.game.state.WaterfallState(); + app.getStateManager().attach(waterfallState); emitterSystem = new de.blight.game.state.EmitterSystem(); app.getStateManager().attach(emitterSystem); + placedSoundSystem = new de.blight.game.state.PlacedSoundSystem(); + app.getStateManager().attach(placedSoundSystem); WorldObjectsState worldObjects = new WorldObjectsState(); worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range); app.getStateManager().attach(worldObjects); @@ -470,7 +475,9 @@ public class WorldScene extends BaseAppState { if (riverSound != null) riverSound.setPlayerPosition(pos); if (ambientSounds != null) ambientSounds.setPlayerPosition(pos); if (musicSystem != null) musicSystem.setPlayerPosition(pos); - if (emitterSystem != null) emitterSystem.setPlayerPosition(pos); + if (emitterSystem != null) emitterSystem.setPlayerPosition(pos); + if (placedSoundSystem != null) placedSoundSystem.setPlayerPosition(pos); + if (waterfallState != null) waterfallState.setPlayerPosition(pos); } if (musicSystem != null && dayNight != null) diff --git a/blight-game/src/main/java/de/blight/game/state/PlacedSoundSystem.java b/blight-game/src/main/java/de/blight/game/state/PlacedSoundSystem.java new file mode 100644 index 0000000..647ea16 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/PlacedSoundSystem.java @@ -0,0 +1,112 @@ +package de.blight.game.state; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import de.blight.common.PlacedSound; +import de.blight.common.SoundIO; +import de.blight.game.audio.StereoSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Lädt platzierte Sound-Quellen (blight_sounds.bls) und spielt sie mit + * korrektem L/R-Panning und Distanz-Fading ab. + * Panning-Logik: {@link StereoSource}. + */ +public class PlacedSoundSystem extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(PlacedSoundSystem.class); + private static final float CHECK_INTERVAL = 0.1f; + + private SimpleApplication app; + private Node rootNode; + private AudioSettingsState audioSettings; + + private final List data = new ArrayList<>(); + private final List sources = new ArrayList<>(); + + private final Vector3f playerPos = new Vector3f(); + private float checkTimer = 0f; + + @Override + protected void initialize(Application application) { + app = (SimpleApplication) application; + rootNode = ((SimpleApplication) application).getRootNode(); + audioSettings = getStateManager().getState(AudioSettingsState.class); + + try { + for (PlacedSound ps : SoundIO.load()) { + if (ps.soundPath() == null || ps.soundPath().isEmpty()) continue; + StereoSource src = StereoSource.tryLoad(app.getAssetManager(), ps.soundPath()); + if (src != null) { + data.add(ps); + sources.add(src); + } + } + if (!data.isEmpty()) log.info("[PlacedSoundSystem] {} Sound-Quellen geladen.", data.size()); + } catch (IOException e) { + log.warn("[PlacedSoundSystem] blight_sounds.bls nicht ladbar: {}", e.getMessage()); + } + } + + @Override + protected void cleanup(Application application) { + for (StereoSource src : sources) src.stop(rootNode); + data.clear(); + sources.clear(); + } + + @Override protected void onEnable() {} + @Override protected void onDisable() {} + + public void setPlayerPosition(Vector3f pos) { playerPos.set(pos); } + + @Override + public void update(float tpf) { + if (data.isEmpty()) return; + + checkTimer += tpf; + if (checkTimer < CHECK_INTERVAL) return; + checkTimer = 0f; + + float ambientVol = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f; + Vector3f soundPos = new Vector3f(); + + for (int i = 0; i < data.size(); i++) { + PlacedSound ps = data.get(i); + StereoSource src = sources.get(i); + + float dx = ps.x() - playerPos.x; + float dz = ps.z() - playerPos.z; + float dist = (float) Math.sqrt(dx * dx + dz * dz); + + float innerR = ps.radius() * 0.33f; + float distFactor; + if (dist <= innerR) distFactor = 1f; + else if (dist >= ps.radius()) distFactor = 0f; + else distFactor = 1f - (dist - innerR) / (ps.radius() - innerR); + + float volume = ps.volume() * ambientVol * distFactor; + + if (volume > 0f && !src.isAttached()) { + src.attachAndPlay(rootNode); + } + + if (src.isAttached()) { + if (volume <= 0f) { + src.stop(rootNode); + } else { + soundPos.set(ps.x(), ps.y(), ps.z()); + src.update(app.getCamera(), soundPos, playerPos, volume); + } + } + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/WaterfallState.java b/blight-game/src/main/java/de/blight/game/state/WaterfallState.java index 486cf83..3536014 100644 --- a/blight-game/src/main/java/de/blight/game/state/WaterfallState.java +++ b/blight-game/src/main/java/de/blight/game/state/WaterfallState.java @@ -22,6 +22,7 @@ import com.jme3.texture.Texture2D; import com.jme3.util.BufferUtils; import de.blight.common.PlacedWaterfall; import de.blight.common.WaterfallIO; +import de.blight.game.audio.StereoSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,11 +39,16 @@ public class WaterfallState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(WaterfallState.class); - private static final int ROWS = 24; // Tessellierungs-Zeilen vertikal - private static final int COLS = 6; // Tessellierungs-Spalten horizontal - private static final float BULGE_MAX = 1.2f; // Maximale horizontale Vorwölbung am oberen Rand (m) - private static final float V_RANGE = 0.30f; // Anteil der Wasserfallhöhe für den Bogen (0..1) - private static final float SPLASH_HEIGHT = 0.9f; // Höhe des vertikalen Schaum-Vorhangs (m) + private static final int ROWS = 24; + private static final int COLS = 6; + private static final float BULGE_MAX = 1.2f; + private static final float V_RANGE = 0.30f; + private static final float SPLASH_HEIGHT = 0.9f; + + private static final String AUDIO_PATH = "audio/ambient/water/waterfall.ogg"; + private static final float AUDIO_RADIUS = 25f; + private static final float AUDIO_INNER_R = 8f; + private static final float AUDIO_VOLUME = 0.8f; private Node rootNode; private final List geos = new ArrayList<>(); @@ -51,12 +57,18 @@ public class WaterfallState extends BaseAppState { private final List foamMaterials = new ArrayList<>(); private final List mists = new ArrayList<>(); private float time = 0f; - private Texture2D softCircleTex = null; // lazily built, shared by alle Emitter + private Texture2D softCircleTex = null; + + private final List audioSources = new ArrayList<>(); + private final List audioPositions = new ArrayList<>(); + private AudioSettingsState audioSettings; + private final Vector3f playerPos = new Vector3f(); @Override - protected void initialize(Application app) { - rootNode = ((SimpleApplication) app).getRootNode(); - AssetManager assets = app.getAssetManager(); + protected void initialize(Application application) { + rootNode = ((SimpleApplication) application).getRootNode(); + AssetManager assets = application.getAssetManager(); + audioSettings = getStateManager().getState(AudioSettingsState.class); List list; try { @@ -70,6 +82,7 @@ public class WaterfallState extends BaseAppState { for (PlacedWaterfall wf : list) { try { buildWaterfall(wf, assets); + buildAudioSource(wf, assets); } catch (Exception e) { log.error("Fehler beim Aufbauen eines Wasserfalls", e); } @@ -77,6 +90,28 @@ public class WaterfallState extends BaseAppState { log.info("{} Wasserfall-Quad(s) geladen.", list.size()); } + private void buildAudioSource(PlacedWaterfall wf, AssetManager assets) { + Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az()); + Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz()); + Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz()); + Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz()); + Vector3f outward = computeOutward(a, b, c, d); + float waterY = (wf.cy() + wf.dy()) * 0.5f; + Vector3f audioPos = c.add(d).multLocal(0.5f).addLocal(outward.mult(BULGE_MAX)); + audioPos.y = waterY; + + StereoSource src = StereoSource.tryLoad(assets, AUDIO_PATH); + if (src != null) { + // Sofort attachen und bei Vol=0 starten — kein späteres Start/Stop. + // Identisches Muster wie OceanSoundState: immer spielend, nur Lautstärke variiert. + src.attachAndPlay(rootNode); + audioSources.add(src); + audioPositions.add(audioPos); + } + } + + public void setPlayerPosition(Vector3f pos) { playerPos.set(pos); } + @Override public void update(float tpf) { if (!materials.isEmpty()) { @@ -89,10 +124,36 @@ public class WaterfallState extends BaseAppState { foamMaterials.get(i).setColor("Color", new ColorRGBA(1f, 1f, 1f, alpha)); } } + + if (audioSources.isEmpty()) return; + + float ambientVol = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f; + + for (int i = 0; i < audioSources.size(); i++) { + StereoSource src = audioSources.get(i); + Vector3f apos = audioPositions.get(i); + + float dx = apos.x - playerPos.x; + float dz = apos.z - playerPos.z; + float dist = (float) Math.sqrt(dx * dx + dz * dz); + + float distFactor; + if (dist <= AUDIO_INNER_R) distFactor = 1f; + else if (dist >= AUDIO_RADIUS) distFactor = 0f; + else distFactor = 1f - (dist - AUDIO_INNER_R) / (AUDIO_RADIUS - AUDIO_INNER_R); + + float volume = AUDIO_VOLUME * ambientVol * distFactor; + + // Panning + Lautstärke jeden Frame — kein Start/Stop, nur Vol=0 außerhalb + src.update(getApplication().getCamera(), apos, playerPos, volume); + } } @Override - protected void cleanup(Application app) { + protected void cleanup(Application application) { + for (StereoSource src : audioSources) src.stop(rootNode); + audioSources.clear(); + audioPositions.clear(); for (Geometry g : geos) g.removeFromParent(); for (Geometry g : foams) g.removeFromParent(); for (ParticleEmitter e : mists) e.removeFromParent(); diff --git a/blight-map/src/main/map/blight_emitters.bpe b/blight-map/src/main/map/blight_emitters.bpe index 966e690..1581edb 100644 --- a/blight-map/src/main/map/blight_emitters.bpe +++ b/blight-map/src/main/map/blight_emitters.bpe @@ -1,2 +1,2 @@ # x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate -0.93604 2.50000 -3.56553 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000 +1.46137 2.00000 -1.08840 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.2739 0.6573 0.0000 0.7500 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000 diff --git a/blight-map/src/main/map/blight_lights.bll b/blight-map/src/main/map/blight_lights.bll index df6aa3b..9865ce4 100644 --- a/blight-map/src/main/map/blight_lights.bll +++ b/blight-map/src/main/map/blight_lights.bll @@ -1,3 +1,2 @@ # x y z r g b intensity radius --2.18687 3.00000 -5.43333 1.00000 1.00000 1.00000 1.00000 20.00000 -0.42349 3.00000 -10.67357 1.00000 1.00000 1.00000 1.00000 20.00000 +1.46137 2.70000 -1.08840 1.00000 0.60000 0.40000 2.00000 8.00000 diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 259d086..042eb5b 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -11,7 +11,6 @@ Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec NONE Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE -Models/imported/wolf.j3o -1.25617 2.00000 -1.86424 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 NONE Models/imported/bank1.j3o 0.30349 2.00000 -5.00573 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH b26f42df-7897-4840-9e7f-5f35329aa0a6 NONE Models/trees/pine/medium/pine_medium_20260706_190939.j3o 157.82524 2.00000 -793.89911 -2.44322 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE Models/trees/pine/medium/pine_medium_20260706_190939.j3o 170.43440 2.00000 -784.16235 2.81244 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 TREE @@ -22,3 +21,4 @@ Models/trees/pine/medium/pine_medium_20260706_190939.j3o 155.19229 2.00000 -802. Models/northcoast/alte_fischerhytte.j3o 317.92999 2.01000 -863.58002 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/northcoast/alte_fischerhytte2.j3o 312.98001 2.00000 -851.37000 -3.50811 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING Models/northcoast/alte_fischerhytte.j3o 322.73001 2.00000 -856.09003 -1.83155 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BUILDING +Models/imported/feuerstelle1.j3o 1.46137 2.00000 -1.08840 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 NONE diff --git a/blight-map/src/main/map/blight_sounds.bls b/blight-map/src/main/map/blight_sounds.bls new file mode 100644 index 0000000..46ac5e2 --- /dev/null +++ b/blight-map/src/main/map/blight_sounds.bls @@ -0,0 +1,2 @@ +# x y z soundPath volume radius +1.46137 2.50000 -1.08840 audio/ambient/fire/campfire.ogg 1.0000 20.0000 diff --git a/licences/Read.txt b/licences/Read.txt new file mode 100644 index 0000000..5932739 --- /dev/null +++ b/licences/Read.txt @@ -0,0 +1,11 @@ + +"Free Sounds Library" + + +Free Sound Effects Site. + + +Licence: License: Attribution 4.0 International (CC BY 4.0). You are allowed to use sound effects free of charge and royalty free in your multimedia projects for commercial or non-commercial purposes. + + +https://www.freesoundslibrary.com/ \ No newline at end of file