Objekte können jetzt Sounds haben (z.B. Lagerfeuer). Sound System optimiert

This commit is contained in:
2026-09-14 16:09:21 +02:00
parent 08c76a798d
commit 45f7ee1045
42 changed files with 1721 additions and 109 deletions

View File

@@ -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();
}
});
});
}

View File

@@ -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));

View File

@@ -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<EmitterClick> 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<SoundClick> 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<SoundPropertyChange> 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<ObjectClick> 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<float[]> removeLightPositions = new ConcurrentLinkedQueue<>();
/** SceneObjectState → EmitterState: Weltpositionen von zu löschenden Emittern. */
public final ConcurrentLinkedQueue<float[]> removeEmitterPositions = new ConcurrentLinkedQueue<>();
/** SceneObjectState → PlacedSoundState: Weltpositionen von zu löschenden Sounds. */
public final ConcurrentLinkedQueue<float[]> 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<de.blight.common.ModelMeta.AttachedLight> modelEditorAttachedLights = java.util.List.of();
public volatile java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorAttachedEmitters = java.util.List.of();
public volatile java.util.List<de.blight.common.ModelMeta.AttachedSound> 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<PathRename> sceneObjectPathRenames = new ConcurrentLinkedQueue<>();
/** EmitterState pollt diese Queue und ersetzt betroffene PlacedEmitter-Records. */
public final ConcurrentLinkedQueue<PathRename> 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 = () -> {};
}

View File

@@ -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<Node> setLeftPanel,
Consumer<Node> setBottomPanel,
Supplier<StackPane> centerStackSupplier,
Consumer<String> setCurrentTool
Consumer<String> setCurrentTool,
Runnable scheduleAutoSave
) {}

View File

@@ -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(); }
}
}

View File

@@ -47,7 +47,8 @@ public class EmitterState extends BaseAppState {
private final List<Node> markers = new ArrayList<>();
private final List<ParticleEmitter> particles = new ArrayList<>();
private int selectedIdx = -1;
private int selectedIdx = -1;
private int lastKnownLayer = -1;
private List<PlacedEmitter> 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 ────────────────────────────────────────────────────────

View File

@@ -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);

View File

@@ -152,6 +152,7 @@ public class ItemPlacementState extends BaseAppState {
if (selectedItemIdx >= 0) {
removeItem(selectedItemIdx);
deselectItem();
input.onObjectDeleted.run();
}
}
}

View File

@@ -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);

View File

@@ -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(); }
}
}

View File

@@ -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<de.blight.common.ModelMeta.AttachedLight> lights,
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters) {
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
java.util.List<de.blight.common.ModelMeta.AttachedSound> 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. */

View File

@@ -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(),

View File

@@ -138,7 +138,7 @@ public class PathNetworkEditorState extends BaseAppState {
if (input.deletePathNodeRequested) {
input.deletePathNodeRequested = false;
deleteSelected();
if (selectedNode != null) { deleteSelected(); input.onObjectDeleted.run(); }
}
}

View File

@@ -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<PlacedSound> sounds = new ArrayList<>();
private final List<Node> markers = new ArrayList<>();
private int selectedIdx = -1;
private List<PlacedSound> 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<PlacedSound> getPlacedSounds() {
return new ArrayList<>(sounds);
}
public void loadPlacedSounds(List<PlacedSound> 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;
}
}

View File

@@ -85,9 +85,13 @@ public class SceneObjectState extends BaseAppState {
private Node rootNode;
private TerrainQuad terrain;
private final List<SceneObject> objects = new ArrayList<>();
private final List<Node> objNodes = new ArrayList<>();
private final List<String> animClips = new ArrayList<>(); // parallel zu objects
private final List<SceneObject> objects = new ArrayList<>();
private final List<Node> objNodes = new ArrayList<>();
private final List<String> animClips = new ArrayList<>(); // parallel zu objects
// Kaskaden-Löschung: Welt-Positionen der mitplatzierten Lichter/Emitter/Sounds pro Objekt
private final List<java.util.List<float[]>> placedLightPositions = new ArrayList<>();
private final List<java.util.List<float[]>> placedEmitterPositions = new ArrayList<>();
private final List<java.util.List<float[]>> 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<de.blight.common.PlacedLight> list =
new java.util.ArrayList<>(de.blight.common.LightIO.load());
java.util.List<float[]> 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<de.blight.common.PlacedEmitter> list =
new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
java.util.List<float[]> 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<de.blight.common.PlacedSound> list =
new java.util.ArrayList<>(de.blight.common.SoundIO.load());
java.util.List<float[]> 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<Integer> sortedDesc = new ArrayList<>(selectedIndices);
sortedDesc.sort(Comparator.reverseOrder());
// Positionen aller Kaskaden-Anhänge sammeln (aus Meta – funktioniert auch nach Neustart)
List<float[]> cascadeLights = new ArrayList<>();
List<float[]> cascadeEmitters = new ArrayList<>();
List<float[]> 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<float[]> positions, String type) {
try {
switch (type) {
case "lights" -> {
List<de.blight.common.PlacedLight> 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<de.blight.common.PlacedEmitter> 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<de.blight.common.PlacedSound> 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 {

View File

@@ -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(); }
}
}

View File

@@ -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<PlacedModel> models = sceneObjState != null ? sceneObjState.getPlacedModels() : null;
final List<PlacedLight> lights = lightState != null ? lightState.getPlacedLights() : null;
final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null;
final List<de.blight.common.PlacedSound> placedSounds = placedSoundState != null ? placedSoundState.getPlacedSounds() : null;
final List<PlacedWater> waters = waterBodyState != null ? waterBodyState.getPlacedBodies() : null;
final List<List<RiverPoint>> rivers = riverEditorState != null ? riverEditorState.getPlacedRivers() : null;
final List<PlacedSoundArea> 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); }

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -58,6 +58,8 @@ public class ModelEditorController implements ToolController {
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
private VBox modelEditorLightBox = null;
private VBox modelEditorEmitterBox = null;
private VBox modelEditorSoundBox = null;
private final java.util.List<de.blight.common.ModelMeta.AttachedSound> modelEditorSounds = new java.util.ArrayList<>();
private ComboBox<de.blight.common.ModelCategory> modelEditorCategoryCB = null;
private ComboBox<de.blight.common.model.InteractableType> modelEditorInteractableCB = null;
private Spinner<Double> 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<de.blight.common.ModelMeta.AttachedLight> lights,
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
java.util.List<de.blight.common.ModelMeta.AttachedSound> 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<Double> ox = buildOffsetSpinner(ae.offsetX());
Spinner<Double> oy = buildOffsetSpinner(ae.offsetY());
Spinner<Double> 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<String> 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<Double> ox = buildOffsetSpinner(as.offsetX());
Spinner<Double> oy = buildOffsetSpinner(as.offsetY());
Spinner<Double> 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<Double> volSpin = new Spinner<>(0.0, 1.0, as.volume(), 0.05);
volSpin.setEditable(true);
addLabeledRow(g, 4, "Lautstärke:", volSpin);
Spinner<Double> 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;
}
}

View File

@@ -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 ->