Weitere Anpassungen bezüglich der Voxel und des Zoning Systems

This commit is contained in:
2026-08-12 23:01:09 +02:00
parent ae3caacebf
commit 400a736804
88 changed files with 2826 additions and 686 deletions

View File

@@ -4,6 +4,7 @@ MaterialDef CloudDome {
Vector2 CloudOffset : 0.0 0.0 Vector2 CloudOffset : 0.0 0.0
Float CloudCover : 0.0 Float CloudCover : 0.0
Color CloudColor : 1.0 1.0 1.0 1.0 Color CloudColor : 1.0 1.0 1.0 1.0
Vector3 SunsetGlow : 0.0 0.0 0.0
} }
Technique { Technique {

View File

@@ -1,6 +1,7 @@
uniform vec2 m_CloudOffset; uniform vec2 m_CloudOffset;
uniform float m_CloudCover; uniform float m_CloudCover;
uniform vec4 m_CloudColor; uniform vec4 m_CloudColor;
uniform vec3 m_SunsetGlow; // additiver Rot-Orange-Ton für Wolkenunterseite bei tiefem Sonnenstand
in vec3 vDir; in vec3 vDir;
out vec4 outColor; out vec4 outColor;
@@ -49,5 +50,8 @@ void main() {
cloud *= hFade; cloud *= hFade;
outColor = vec4(m_CloudColor.rgb, cloud * m_CloudColor.a); // Abendrot-Glut auf Unterseite: overhead-Wolken (dir.y hoch) leuchten stärker rot
float glowWeight = smoothstep(0.0, 0.4, dir.y);
vec3 cloudRgb = m_CloudColor.rgb + m_SunsetGlow * glowWeight;
outColor = vec4(min(cloudRgb, vec3(1.0)), cloud * m_CloudColor.a);
} }

View File

@@ -0,0 +1,10 @@
package de.blight.common;
public record AreaDefinition(
String id,
String dayTrack,
String nightTrack,
String combatTrack
) {
public String nameKey() { return id.isBlank() ? "" : id + ".name"; }
}

View File

@@ -0,0 +1,47 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
public final class AreaDefinitionIO {
private AreaDefinitionIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_area_definitions.bad");
}
public static void save(List<AreaDefinition> defs) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# id\tdayTrack\tnightTrack\tcombatTrack");
w.newLine();
for (AreaDefinition d : defs) {
w.write(d.id());
w.write('\t');
w.write(d.dayTrack());
w.write('\t');
w.write(d.nightTrack());
w.write('\t');
w.write(d.combatTrack());
w.newLine();
}
}
}
public static List<AreaDefinition> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<AreaDefinition> list = new ArrayList<>();
for (String line : Files.readAllLines(p)) {
line = line.stripLeading();
if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1);
if (f.length < 4) continue;
list.add(new AreaDefinition(f[0].strip(), f[1].strip(), f[2].strip(), f[3].strip()));
}
return list;
}
}

View File

@@ -1,5 +1,8 @@
package de.blight.common; package de.blight.common;
import de.blight.common.model.trigger.Trigger;
import de.blight.common.model.trigger.TriggerIO;
import java.io.*; import java.io.*;
import java.nio.file.*; import java.nio.file.*;
import java.util.*; import java.util.*;
@@ -16,18 +19,14 @@ public final class AreaIO {
Path p = getPath(); Path p = getPath();
Files.createDirectories(p.getParent()); Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) { try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# polygon\tnameId\tdayTrack\tnightTrack\tcombatTrack"); w.write("# polygon\tareaId\ttriggersJson");
w.newLine(); w.newLine();
for (PlacedArea a : areas) { for (PlacedArea a : areas) {
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ())); w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
w.write('\t'); w.write('\t');
w.write(a.nameId()); w.write(a.areaId());
w.write('\t'); w.write('\t');
w.write(a.dayTrack()); w.write(TriggerIO.serializeList(a.triggers()));
w.write('\t');
w.write(a.nightTrack());
w.write('\t');
w.write(a.combatTrack());
w.newLine(); w.newLine();
} }
} }
@@ -38,14 +37,16 @@ public final class AreaIO {
if (!Files.exists(p)) return List.of(); if (!Files.exists(p)) return List.of();
List<PlacedArea> list = new ArrayList<>(); List<PlacedArea> list = new ArrayList<>();
for (String line : Files.readAllLines(p)) { for (String line : Files.readAllLines(p)) {
line = line.strip(); line = line.stripLeading();
if (line.isEmpty() || line.startsWith("#")) continue; if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1); String[] f = line.split("\t", -1);
if (f.length < 5) continue; if (f.length < 1) continue;
try { try {
float[][] pts = SoundAreaIO.decodePolygon(f[0]); float[][] pts = SoundAreaIO.decodePolygon(f[0]);
if (pts[0].length < 3) continue; if (pts[0].length < 3) continue;
list.add(new PlacedArea(pts[0], pts[1], f[1], f[2], f[3], f[4])); String areaId = f.length > 1 ? f[1].strip() : "";
List<Trigger> triggers = f.length > 2 ? TriggerIO.deserializeList(f[2]) : new ArrayList<>();
list.add(new PlacedArea(pts[0], pts[1], areaId, triggers));
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
return list; return list;

View File

@@ -1,10 +1,16 @@
package de.blight.common; package de.blight.common;
import de.blight.common.model.trigger.Trigger;
import java.util.List;
public record PlacedArea( public record PlacedArea(
float[] pointsX, float[] pointsX,
float[] pointsZ, float[] pointsZ,
String nameId, String areaId,
String dayTrack, List<Trigger> triggers
String nightTrack, ) {
String combatTrack public PlacedArea(float[] pointsX, float[] pointsZ, String areaId) {
) {} this(pointsX, pointsZ, areaId, List.of());
}
}

View File

@@ -28,6 +28,6 @@ public class Location {
public void entered(MainCharacter character) { public void entered(MainCharacter character) {
triggers.stream() triggers.stream()
.filter(t -> t.isTriggarable(character)) .filter(t -> t.isTriggarable(character))
.forEach(t -> t.trigger(character)); .forEach(t -> t.fire(character));
} }
} }

View File

@@ -2,10 +2,13 @@ package de.blight.common.model;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import de.blight.common.model.quests.Quest; import de.blight.common.model.quests.Quest;
import lombok.AccessLevel; import lombok.AccessLevel;
@@ -44,6 +47,18 @@ public class MainCharacter extends GameCharacter {
/** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */ /** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */
private Set<String> playedMonologueIds = new HashSet<>(); private Set<String> playedMonologueIds = new HashSet<>();
/** IDs bereits gefeuerter Trigger – verhindert Mehrfach-Auslösung. */
private Set<String> firedTriggerIds = new HashSet<>();
/** Bereits betretene Zonen (für FirstTimeCondition). */
private Set<String> visitedZoneIds = new HashSet<>();
/** Freie Spielvariablen (key→value) für GameVariableCondition. */
private Map<String, String> gameVariables = new HashMap<>();
/** Fraktionen, denen der Spieler angehört (für FactionMemberCondition). */
private Set<UUID> memberFractionIds = new HashSet<>();
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private List<CharacterListener> listeners = new ArrayList<CharacterListener>(); private List<CharacterListener> listeners = new ArrayList<CharacterListener>();
@@ -53,6 +68,11 @@ public class MainCharacter extends GameCharacter {
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private transient Queue<Monologue> pendingMonologues = new ArrayDeque<>(); private transient Queue<Monologue> pendingMonologues = new ArrayDeque<>();
/** Warteschlange für per Trigger ausgelöste NPC-Dialoge – wird NICHT serialisiert. */
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient Queue<String> pendingDialogNpcIds = new ArrayDeque<>();
public void handleDialogOption(DialogOption option) { public void handleDialogOption(DialogOption option) {
if (option.getRequiredItem() != null) { if (option.getRequiredItem() != null) {
getInventar().remove(option.getRequiredItem()); getInventar().remove(option.getRequiredItem());
@@ -146,6 +166,37 @@ public class MainCharacter extends GameCharacter {
return pendingMonologues.poll(); return pendingMonologues.poll();
} }
/** Stellt einen per Trigger ausgelösten NPC-Dialog in die Warteschlange. */
public void queueDialog(String npcId) {
if (pendingDialogNpcIds == null) pendingDialogNpcIds = new ArrayDeque<>();
pendingDialogNpcIds.offer(npcId);
}
/** Gibt den nächsten ausstehenden Dialog-NPC zurück oder null. */
public String pollPendingDialog() {
if (pendingDialogNpcIds == null) return null;
return pendingDialogNpcIds.poll();
}
public boolean isQuestCompleted(String questId) {
return completedQuests != null && completedQuests.stream()
.anyMatch(q -> questId.equals(q.getQuestId()));
}
public boolean isQuestAccepted(String questId) {
return openQuests != null && openQuests.stream()
.anyMatch(q -> questId.equals(q.getQuestId()));
}
public boolean isQuestRejected(String questId) {
return abortedQuests != null && abortedQuests.stream()
.anyMatch(q -> questId.equals(q.getQuestId()));
}
public boolean isFactionMember(UUID fractionId) {
return memberFractionIds != null && memberFractionIds.contains(fractionId);
}
/** Wendet die Quest-Folgen eines Monologs an. */ /** Wendet die Quest-Folgen eines Monologs an. */
public void handleMonologue(Monologue m) { public void handleMonologue(Monologue m) {
if (m.getRecievesQuest() != null) startQuest(m.getRecievesQuest()); if (m.getRecievesQuest() != null) startQuest(m.getRecievesQuest());

View File

@@ -0,0 +1,16 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ChapterCondition extends Condition {
private int minChapter;
@Override
public boolean evaluate(MainCharacter character) {
return character.getChapter() >= minChapter;
}
}

View File

@@ -0,0 +1,7 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
public abstract class Condition {
public abstract boolean evaluate(MainCharacter character);
}

View File

@@ -0,0 +1,3 @@
package de.blight.common.model.trigger;
public enum ConditionMode { ALL, ANY }

View File

@@ -0,0 +1,19 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
import java.util.UUID;
@Getter
@Setter
public class FactionMemberCondition extends Condition {
private UUID fractionId;
@Override
public boolean evaluate(MainCharacter character) {
if (fractionId == null) return false;
return character.isFactionMember(fractionId);
}
}

View File

@@ -0,0 +1,16 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FirstTimeCondition extends Condition {
private String zoneId;
@Override
public boolean evaluate(MainCharacter character) {
return zoneId != null && !character.getVisitedZoneIds().contains(zoneId);
}
}

View File

@@ -0,0 +1,20 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class GameVariableCondition extends Condition {
private String key;
private String value;
@Override
public boolean evaluate(MainCharacter character) {
if (key == null || key.isBlank()) return false;
String actual = character.getGameVariables().get(key);
if (value == null || value.isBlank()) return actual != null;
return value.equals(actual);
}
}

View File

@@ -0,0 +1,23 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class NpcDialogTrigger extends Trigger {
private String npcId;
@Override
public boolean isTriggarableDelegate(MainCharacter character) {
return npcId != null && !npcId.isBlank();
}
@Override
public void trigger(MainCharacter character) {
if (npcId != null && !npcId.isBlank()) {
character.queueDialog(npcId);
}
}
}

View File

@@ -0,0 +1,16 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QuestAcceptedCondition extends Condition {
private String questId;
@Override
public boolean evaluate(MainCharacter character) {
return questId != null && character.isQuestAccepted(questId);
}
}

View File

@@ -0,0 +1,16 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QuestCompletedCondition extends Condition {
private String questId;
@Override
public boolean evaluate(MainCharacter character) {
return questId != null && character.isQuestCompleted(questId);
}
}

View File

@@ -0,0 +1,16 @@
package de.blight.common.model.trigger;
import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QuestRejectedCondition extends Condition {
private String questId;
@Override
public boolean evaluate(MainCharacter character) {
return questId != null && character.isQuestRejected(questId);
}
}

View File

@@ -4,17 +4,38 @@ import de.blight.common.model.MainCharacter;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter @Getter
@Setter @Setter
public abstract class Trigger { public abstract class Trigger {
private int requiresChapter; private String triggerId = UUID.randomUUID().toString();
private int requiresChapter;
private ConditionMode conditionMode = ConditionMode.ALL;
private List<Condition> conditions = new ArrayList<>();
public boolean isTriggarable(MainCharacter character) { public boolean isTriggarable(MainCharacter character) {
return character.getChapter() >= requiresChapter && isTriggarableDelegate(character); if (triggerId != null && character.getFiredTriggerIds().contains(triggerId)) return false;
} if (character.getChapter() < requiresChapter) return false;
if (!conditions.isEmpty()) {
if (conditionMode == ConditionMode.ALL) {
if (!conditions.stream().allMatch(c -> c.evaluate(character))) return false;
} else {
if (conditions.stream().noneMatch(c -> c.evaluate(character))) return false;
}
}
return isTriggarableDelegate(character);
}
public abstract boolean isTriggarableDelegate(MainCharacter character); public final void fire(MainCharacter character) {
trigger(character);
if (triggerId != null) character.getFiredTriggerIds().add(triggerId);
}
public abstract void trigger(MainCharacter character); public abstract boolean isTriggarableDelegate(MainCharacter character);
public abstract void trigger(MainCharacter character);
} }

View File

@@ -9,19 +9,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/**
* Gson-Serialisierung für {@link Trigger}-Instanzen mit Typ-Diskriminator.
*
* JSON-Format (kompakt, kein Pretty-Print für Inline-Verwendung in z. B. LocationZoneIO):
* {@code [{"type":"QUEST_START","requiresChapter":0,"questId":"my_quest"}, ...]}
*
* Bekannte Typen:
* <ul>
* <li>{@code QUEST_START} → {@link QuestStartTrigger}</li>
* <li>{@code NPC_STATUS} → {@link NpcStatusTrigger}</li>
* <li>{@code FRACTION_STATUS} → {@link FractionStatusTrigger}</li>
* </ul>
*/
public final class TriggerIO { public final class TriggerIO {
public static final String TYPE_QUEST_START = "QUEST_START"; public static final String TYPE_QUEST_START = "QUEST_START";
@@ -29,6 +16,7 @@ public final class TriggerIO {
public static final String TYPE_FRACTION_STATUS = "FRACTION_STATUS"; public static final String TYPE_FRACTION_STATUS = "FRACTION_STATUS";
public static final String TYPE_CHANGE_ROUTINE = "CHANGE_ROUTINE"; public static final String TYPE_CHANGE_ROUTINE = "CHANGE_ROUTINE";
public static final String TYPE_MONOLOGUE = "MONOLOGUE"; public static final String TYPE_MONOLOGUE = "MONOLOGUE";
public static final String TYPE_NPC_DIALOG = "NPC_DIALOG";
private static final Gson GSON = new GsonBuilder() private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter()) .registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter())
@@ -36,18 +24,15 @@ public final class TriggerIO {
private TriggerIO() {} private TriggerIO() {}
/** Registriert den Trigger-Typ-Adapter an einem GsonBuilder (für andere Module). */
public static GsonBuilder registerAdapters(GsonBuilder builder) { public static GsonBuilder registerAdapters(GsonBuilder builder) {
return builder.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter()); return builder.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter());
} }
/** Serialisiert eine Trigger-Liste als kompaktes JSON (kein Zeilenumbruch). */
public static String serializeList(List<Trigger> triggers) { public static String serializeList(List<Trigger> triggers) {
if (triggers == null || triggers.isEmpty()) return "[]"; if (triggers == null || triggers.isEmpty()) return "[]";
return GSON.toJson(triggers); return GSON.toJson(triggers);
} }
/** Deserialisiert eine Trigger-Liste aus JSON. Gibt leere Liste bei Fehler zurück. */
public static List<Trigger> deserializeList(String json) { public static List<Trigger> deserializeList(String json) {
if (json == null || json.isBlank() || "[]".equals(json.strip())) return new ArrayList<>(); if (json == null || json.isBlank() || "[]".equals(json.strip())) return new ArrayList<>();
try { try {
@@ -71,7 +56,13 @@ public final class TriggerIO {
public JsonElement serialize(Trigger src, Type typeOfSrc, JsonSerializationContext ctx) { public JsonElement serialize(Trigger src, Type typeOfSrc, JsonSerializationContext ctx) {
JsonObject obj = new JsonObject(); JsonObject obj = new JsonObject();
obj.addProperty("type", typeOf(src)); obj.addProperty("type", typeOf(src));
if (src.getTriggerId() != null) obj.addProperty("triggerId", src.getTriggerId());
obj.addProperty("requiresChapter", src.getRequiresChapter()); obj.addProperty("requiresChapter", src.getRequiresChapter());
obj.addProperty("conditionMode", src.getConditionMode().name());
JsonArray condArr = new JsonArray();
for (Condition c : src.getConditions()) condArr.add(serializeCondition(c));
obj.add("conditions", condArr);
if (src instanceof QuestStartTrigger q) { if (src instanceof QuestStartTrigger q) {
if (q.getQuest() != null && q.getQuest().getQuestId() != null) if (q.getQuest() != null && q.getQuest().getQuestId() != null)
@@ -86,10 +77,12 @@ public final class TriggerIO {
if (f.getTargetStatus() != null) if (f.getTargetStatus() != null)
obj.addProperty("targetStatus", f.getTargetStatus().name()); obj.addProperty("targetStatus", f.getTargetStatus().name());
} else if (src instanceof ChangeRoutineTrigger r) { } else if (src instanceof ChangeRoutineTrigger r) {
if (r.getNpcId() != null) obj.addProperty("npcId", r.getNpcId()); if (r.getNpcId() != null) obj.addProperty("npcId", r.getNpcId());
if (r.getRoutineName() != null) obj.addProperty("routineName", r.getRoutineName()); if (r.getRoutineName() != null) obj.addProperty("routineName", r.getRoutineName());
} else if (src instanceof MonologueTrigger mo) { } else if (src instanceof MonologueTrigger mo) {
if (mo.getMonologueId() != null) obj.addProperty("monologueId", mo.getMonologueId()); if (mo.getMonologueId() != null) obj.addProperty("monologueId", mo.getMonologueId());
} else if (src instanceof NpcDialogTrigger nd) {
if (nd.getNpcId() != null) obj.addProperty("npcId", nd.getNpcId());
} }
return obj; return obj;
} }
@@ -136,20 +129,117 @@ public final class TriggerIO {
if (obj.has("monologueId")) mo.setMonologueId(obj.get("monologueId").getAsString()); if (obj.has("monologueId")) mo.setMonologueId(obj.get("monologueId").getAsString());
yield mo; yield mo;
} }
case TYPE_NPC_DIALOG -> {
NpcDialogTrigger nd = new NpcDialogTrigger();
if (obj.has("npcId")) nd.setNpcId(obj.get("npcId").getAsString());
yield nd;
}
default -> null; default -> null;
}; };
if (t != null && obj.has("requiresChapter")) if (t == null) return null;
t.setRequiresChapter(obj.get("requiresChapter").getAsInt()); if (obj.has("triggerId")) t.setTriggerId(obj.get("triggerId").getAsString());
if (obj.has("requiresChapter")) t.setRequiresChapter(obj.get("requiresChapter").getAsInt());
if (obj.has("conditionMode")) {
try { t.setConditionMode(ConditionMode.valueOf(obj.get("conditionMode").getAsString())); }
catch (IllegalArgumentException ignored) {}
}
if (obj.has("conditions")) {
List<Condition> conds = new ArrayList<>();
for (JsonElement el : obj.getAsJsonArray("conditions")) {
Condition c = deserializeCondition(el.getAsJsonObject());
if (c != null) conds.add(c);
}
t.setConditions(conds);
}
return t; return t;
} }
// ── Condition helpers ─────────────────────────────────────────────────
private static JsonObject serializeCondition(Condition c) {
JsonObject o = new JsonObject();
if (c instanceof ChapterCondition cc) {
o.addProperty("type", "CHAPTER");
o.addProperty("minChapter", cc.getMinChapter());
} else if (c instanceof FirstTimeCondition ft) {
o.addProperty("type", "FIRST_TIME");
if (ft.getZoneId() != null) o.addProperty("zoneId", ft.getZoneId());
} else if (c instanceof QuestCompletedCondition qc) {
o.addProperty("type", "QUEST_COMPLETED");
if (qc.getQuestId() != null) o.addProperty("questId", qc.getQuestId());
} else if (c instanceof QuestAcceptedCondition qa) {
o.addProperty("type", "QUEST_ACCEPTED");
if (qa.getQuestId() != null) o.addProperty("questId", qa.getQuestId());
} else if (c instanceof QuestRejectedCondition qr) {
o.addProperty("type", "QUEST_REJECTED");
if (qr.getQuestId() != null) o.addProperty("questId", qr.getQuestId());
} else if (c instanceof GameVariableCondition gv) {
o.addProperty("type", "GAME_VARIABLE");
if (gv.getKey() != null) o.addProperty("key", gv.getKey());
if (gv.getValue() != null) o.addProperty("value", gv.getValue());
} else if (c instanceof FactionMemberCondition fm) {
o.addProperty("type", "FACTION_MEMBER");
if (fm.getFractionId() != null) o.addProperty("fractionId", fm.getFractionId().toString());
}
return o;
}
private static Condition deserializeCondition(JsonObject o) {
String type = o.has("type") ? o.get("type").getAsString() : "";
return switch (type) {
case "CHAPTER" -> {
ChapterCondition c = new ChapterCondition();
if (o.has("minChapter")) c.setMinChapter(o.get("minChapter").getAsInt());
yield c;
}
case "FIRST_TIME" -> {
FirstTimeCondition c = new FirstTimeCondition();
if (o.has("zoneId")) c.setZoneId(o.get("zoneId").getAsString());
yield c;
}
case "QUEST_COMPLETED" -> {
QuestCompletedCondition c = new QuestCompletedCondition();
if (o.has("questId")) c.setQuestId(o.get("questId").getAsString());
yield c;
}
case "QUEST_ACCEPTED" -> {
QuestAcceptedCondition c = new QuestAcceptedCondition();
if (o.has("questId")) c.setQuestId(o.get("questId").getAsString());
yield c;
}
case "QUEST_REJECTED" -> {
QuestRejectedCondition c = new QuestRejectedCondition();
if (o.has("questId")) c.setQuestId(o.get("questId").getAsString());
yield c;
}
case "GAME_VARIABLE" -> {
GameVariableCondition c = new GameVariableCondition();
if (o.has("key")) c.setKey(o.get("key").getAsString());
if (o.has("value")) c.setValue(o.get("value").getAsString());
yield c;
}
case "FACTION_MEMBER" -> {
FactionMemberCondition c = new FactionMemberCondition();
if (o.has("fractionId")) {
try { c.setFractionId(UUID.fromString(o.get("fractionId").getAsString())); }
catch (IllegalArgumentException ignored) {}
}
yield c;
}
default -> null;
};
}
// ── Type helpers ──────────────────────────────────────────────────────
private static String typeOf(Trigger t) { private static String typeOf(Trigger t) {
if (t instanceof QuestStartTrigger) return TYPE_QUEST_START; if (t instanceof QuestStartTrigger) return TYPE_QUEST_START;
if (t instanceof NpcStatusTrigger) return TYPE_NPC_STATUS; if (t instanceof NpcStatusTrigger) return TYPE_NPC_STATUS;
if (t instanceof FractionStatusTrigger) return TYPE_FRACTION_STATUS; if (t instanceof FractionStatusTrigger) return TYPE_FRACTION_STATUS;
if (t instanceof ChangeRoutineTrigger) return TYPE_CHANGE_ROUTINE; if (t instanceof ChangeRoutineTrigger) return TYPE_CHANGE_ROUTINE;
if (t instanceof MonologueTrigger) return TYPE_MONOLOGUE; if (t instanceof MonologueTrigger) return TYPE_MONOLOGUE;
if (t instanceof NpcDialogTrigger) return TYPE_NPC_DIALOG;
return "UNKNOWN"; return "UNKNOWN";
} }

View File

@@ -6,7 +6,7 @@ plugins {
javafx { javafx {
version = '26' version = '26'
modules = ['javafx.controls', 'javafx.swing'] modules = ['javafx.controls', 'javafx.swing', 'javafx.media']
} }
application { application {

View File

@@ -180,6 +180,7 @@ public class AudioPreviewPopup {
private void buildStage() { private void buildStage() {
stage = new Stage(StageStyle.UTILITY); stage = new Stage(StageStyle.UTILITY);
stage.setTitle("Audio-Vorschau"); stage.setTitle("Audio-Vorschau");
stage.initOwner(de.blight.editor.ui.Dialogs.primaryWindow());
stage.setAlwaysOnTop(true); stage.setAlwaysOnTop(true);
stage.setResizable(false); stage.setResizable(false);
stage.setOnCloseRequest(e -> stopClip()); stage.setOnCloseRequest(e -> stopClip());

View File

@@ -240,6 +240,10 @@ public class EditorApp extends Application {
// Location-Zonen-Werkzeug-Zustand // Location-Zonen-Werkzeug-Zustand
private VBox locationZoneDynamicContent; private VBox locationZoneDynamicContent;
// Zonen-Werkzeug-Zustand (vereint Sound, Area, Location)
private VBox zonenDynamicContent;
private Runnable zonenKindStyleUpdater;
// Spiel-Starten-Werkzeug-Zustand // Spiel-Starten-Werkzeug-Zustand
private TextField spawnXField; private TextField spawnXField;
private TextField spawnZField; private TextField spawnZField;
@@ -324,9 +328,7 @@ public class EditorApp extends Application {
private ToggleButton emitterBtn; private ToggleButton emitterBtn;
private ToggleButton waterBtn; private ToggleButton waterBtn;
private ToggleButton riverBtn; private ToggleButton riverBtn;
private ToggleButton soundAreaBtn; private ToggleButton zonenBtn;
private ToggleButton areaBtn;
private ToggleButton locationZoneBtn;
private ToggleButton playToolBtn; private ToggleButton playToolBtn;
private ToggleButton voxelBtn; private ToggleButton voxelBtn;
private ToggleButton voxelCliffBtn; private ToggleButton voxelCliffBtn;
@@ -350,6 +352,8 @@ public class EditorApp extends Application {
private double editPressX, editPressY; private double editPressX, editPressY;
private int editPressAction; private int editPressAction;
private javafx.animation.Timeline editTimer; private javafx.animation.Timeline editTimer;
private final javafx.animation.PauseTransition worldAutoSave =
new javafx.animation.PauseTransition(javafx.util.Duration.seconds(8));
// Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein) // Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein)
private TreeItem<String> assetTreeRoot; private TreeItem<String> assetTreeRoot;
@@ -672,7 +676,7 @@ public class EditorApp extends Application {
if (input.areaOverlapRejected) { if (input.areaOverlapRejected) {
input.areaOverlapRejected = false; input.areaOverlapRejected = false;
setStatus("Bereich abgelehnt: überschneidet einen bestehenden Bereich."); setStatus("Area abgelehnt: überschneidet eine bestehende Area.");
} }
if (input.locationZoneSelectionChanged) { if (input.locationZoneSelectionChanged) {
@@ -680,6 +684,12 @@ public class EditorApp extends Application {
updateLocationZonePanel(input.selectedLocationZoneInfo); updateLocationZonePanel(input.selectedLocationZoneInfo);
} }
if (input.zonenSelectionChanged) {
input.zonenSelectionChanged = false;
updateZonenPanel(input.zonenSelectedKind);
if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run();
}
if (input.cliffZoneSelectionChanged) { if (input.cliffZoneSelectionChanged) {
input.cliffZoneSelectionChanged = false; input.cliffZoneSelectionChanged = false;
updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0); updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0);
@@ -852,16 +862,12 @@ public class EditorApp extends Application {
statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE); statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE);
statusPoller.play(); statusPoller.play();
javafx.animation.Timeline autoSave = new javafx.animation.Timeline( worldAutoSave.setOnFinished(ev -> {
new javafx.animation.KeyFrame(javafx.util.Duration.seconds(60), ev -> { if (!input.saveRequested) {
if (!input.saveRequested) { input.saveRequested = true;
input.saveRequested = true; setStatus("Auto-Speicherung…");
setStatus("Auto-Speicherung…"); }
} });
})
);
autoSave.setCycleCount(javafx.animation.Timeline.INDEFINITE);
autoSave.play();
} }
// ── Modus-Wechsel ──────────────────────────────────────────────────────── // ── Modus-Wechsel ────────────────────────────────────────────────────────
@@ -1345,6 +1351,7 @@ public class EditorApp extends Application {
MenuItem ctEditItem = new MenuItem("Crafting-Table-Manager"); MenuItem ctEditItem = new MenuItem("Crafting-Table-Manager");
MenuItem fractionEditItem = new MenuItem("Fraktionen-Manager"); MenuItem fractionEditItem = new MenuItem("Fraktionen-Manager");
MenuItem locationEditItem = new MenuItem("Locations-Manager"); MenuItem locationEditItem = new MenuItem("Locations-Manager");
MenuItem areaEditItem = new MenuItem("Areas-Manager");
MenuItem localizationEditItem = new MenuItem("Lokalisierungs-Editor"); MenuItem localizationEditItem = new MenuItem("Lokalisierungs-Editor");
vegetationsItem.setOnAction(e -> switchToVegetationGenerator()); vegetationsItem.setOnAction(e -> switchToVegetationGenerator());
ezTreeItem.setOnAction(e -> switchToEzTree()); ezTreeItem.setOnAction(e -> switchToEzTree());
@@ -1359,11 +1366,12 @@ public class EditorApp extends Application {
ctEditItem.setOnAction(e -> switchToCraftingTableEditor()); ctEditItem.setOnAction(e -> switchToCraftingTableEditor());
fractionEditItem.setOnAction(e -> switchToFractionEditor()); fractionEditItem.setOnAction(e -> switchToFractionEditor());
locationEditItem.setOnAction(e -> switchToLocationEditor()); locationEditItem.setOnAction(e -> switchToLocationEditor());
areaEditItem.setOnAction(e -> switchToAreaEditor());
localizationEditItem.setOnAction(e -> switchToLocalizationEditor()); localizationEditItem.setOnAction(e -> switchToLocalizationEditor());
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem, toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
animPrevItem, objEditorItem, worldEditItem, charEditItem, animPrevItem, objEditorItem, worldEditItem, charEditItem,
questEditItem, itemEditItem, recipeEditItem, ctEditItem, fractionEditItem, questEditItem, itemEditItem, recipeEditItem, ctEditItem, fractionEditItem,
locationEditItem, localizationEditItem); locationEditItem, areaEditItem, localizationEditItem);
Menu viewMenu = new Menu("Ansicht"); Menu viewMenu = new Menu("Ansicht");
MenuItem resetCam = new MenuItem("Kamera zurücksetzen"); MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
@@ -1425,9 +1433,7 @@ public class EditorApp extends Application {
emitterBtn = new ToggleButton("🔥 Emitter"); emitterBtn = new ToggleButton("🔥 Emitter");
waterBtn = new ToggleButton("💧 Wasser"); waterBtn = new ToggleButton("💧 Wasser");
riverBtn = new ToggleButton("↯ Wasserfall"); riverBtn = new ToggleButton("↯ Wasserfall");
soundAreaBtn = new ToggleButton("🔊 Sound"); zonenBtn = new ToggleButton("🗺 Zonen");
areaBtn = new ToggleButton("🗺 Bereiche");
locationZoneBtn = new ToggleButton("📍 Locations");
playToolBtn = new ToggleButton("🎮 Spielen"); playToolBtn = new ToggleButton("🎮 Spielen");
voxelBtn = new ToggleButton("⬡ Voxel"); voxelBtn = new ToggleButton("⬡ Voxel");
voxelCliffBtn = new ToggleButton("⛰ Klippe"); voxelCliffBtn = new ToggleButton("⛰ Klippe");
@@ -1442,9 +1448,7 @@ public class EditorApp extends Application {
emitterBtn.setStyle("-fx-font-weight:bold;"); emitterBtn.setStyle("-fx-font-weight:bold;");
waterBtn.setStyle("-fx-font-weight:bold;"); waterBtn.setStyle("-fx-font-weight:bold;");
riverBtn.setStyle("-fx-font-weight:bold;"); riverBtn.setStyle("-fx-font-weight:bold;");
soundAreaBtn.setStyle("-fx-font-weight:bold;"); zonenBtn.setStyle("-fx-font-weight:bold;");
areaBtn.setStyle("-fx-font-weight:bold;");
locationZoneBtn.setStyle("-fx-font-weight:bold;");
playToolBtn.setStyle("-fx-font-weight:bold;"); playToolBtn.setStyle("-fx-font-weight:bold;");
voxelBtn.setStyle("-fx-font-weight:bold;"); voxelBtn.setStyle("-fx-font-weight:bold;");
voxelCliffBtn.setStyle("-fx-font-weight:bold;"); voxelCliffBtn.setStyle("-fx-font-weight:bold;");
@@ -1461,9 +1465,7 @@ public class EditorApp extends Application {
emitterBtn.setToggleGroup(layerGroup); emitterBtn.setToggleGroup(layerGroup);
waterBtn.setToggleGroup(layerGroup); waterBtn.setToggleGroup(layerGroup);
riverBtn.setToggleGroup(layerGroup); riverBtn.setToggleGroup(layerGroup);
soundAreaBtn.setToggleGroup(layerGroup); zonenBtn.setToggleGroup(layerGroup);
areaBtn.setToggleGroup(layerGroup);
locationZoneBtn.setToggleGroup(layerGroup);
playToolBtn.setToggleGroup(layerGroup); playToolBtn.setToggleGroup(layerGroup);
voxelBtn.setToggleGroup(layerGroup); voxelBtn.setToggleGroup(layerGroup);
voxelCliffBtn.setToggleGroup(layerGroup); voxelCliffBtn.setToggleGroup(layerGroup);
@@ -1523,17 +1525,9 @@ public class EditorApp extends Application {
input.activeLayer = SharedInput.LAYER_WATERFALL; input.activeLayer = SharedInput.LAYER_WATERFALL;
root.setRight(buildWaterfallPanel()); root.setRight(buildWaterfallPanel());
}); });
soundAreaBtn.setOnAction(e -> { zonenBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_SOUND_AREAS; input.activeLayer = SharedInput.LAYER_ZONEN;
root.setRight(buildSoundAreaPanel()); root.setRight(buildZonenPanel());
});
areaBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_AREAS;
root.setRight(buildAreaPanel());
});
locationZoneBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_LOCATION_ZONES;
root.setRight(buildLocationZonePanel());
}); });
playToolBtn.setOnAction(e -> { playToolBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_PLAY_TOOL; input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
@@ -1574,7 +1568,7 @@ public class EditorApp extends Application {
new Separator(Orientation.VERTICAL), emitterBtn, new Separator(Orientation.VERTICAL), emitterBtn,
new Separator(Orientation.VERTICAL), waterBtn, new Separator(Orientation.VERTICAL), waterBtn,
new Separator(Orientation.VERTICAL), riverBtn, new Separator(Orientation.VERTICAL), riverBtn,
new Separator(Orientation.VERTICAL), soundAreaBtn, areaBtn, locationZoneBtn, new Separator(Orientation.VERTICAL), zonenBtn,
new Separator(Orientation.VERTICAL), playToolBtn, new Separator(Orientation.VERTICAL), playToolBtn,
new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn, new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn,
new Separator(Orientation.VERTICAL), stoneBtn, new Separator(Orientation.VERTICAL), stoneBtn,
@@ -4643,9 +4637,9 @@ public class EditorApp extends Application {
case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); } case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); }
case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); } case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); }
case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; root.setRight(buildWaterPanel()); } case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; root.setRight(buildWaterPanel()); }
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_SOUND_AREAS; root.setRight(buildSoundAreaPanel()); } case "soundarea" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "sound"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
case "area" -> { input.activeLayer = SharedInput.LAYER_AREAS; root.setRight(buildAreaPanel()); } case "area" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "area"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
case "locationzone" -> { input.activeLayer = SharedInput.LAYER_LOCATION_ZONES; root.setRight(buildLocationZonePanel()); } case "locationzone" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "location"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(buildWaterfallPanel()); } case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(buildWaterfallPanel()); }
} }
}, },
@@ -4707,6 +4701,7 @@ public class EditorApp extends Application {
input.reloadPlacedOther = true; input.reloadPlacedOther = true;
} }
} }
scheduleAutoSave();
} }
); );
@@ -7857,19 +7852,30 @@ public class EditorApp extends Application {
} else if (bothDown) { } else if (bothDown) {
stopEditTimer(); stopEditTimer();
} else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) { } else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|| input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES;
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
input.playToolClickQueue.offer( input.playToolClickQueue.offer(
new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY())); new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY()));
} else if (singleClickLayer) {
submitEdit(e.getX(), e.getY(), +1);
} else { } else {
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1; editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
submitEdit(editPressX, editPressY, editPressAction); submitEdit(editPressX, editPressY, editPressAction);
startEditTimer(); startEditTimer();
} }
} else if (e.getButton() == MouseButton.SECONDARY) { } else if (e.getButton() == MouseButton.SECONDARY) {
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES;
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1; editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
submitEdit(editPressX, editPressY, editPressAction); submitEdit(editPressX, editPressY, editPressAction);
startEditTimer(); if (!singleClickLayer) startEditTimer();
} }
}); });
@@ -7969,6 +7975,8 @@ public class EditorApp extends Application {
if (editTimer != null) { editTimer.stop(); editTimer = null; } if (editTimer != null) { editTimer.stop(); editTimer = null; }
} }
private void scheduleAutoSave() { worldAutoSave.playFromStart(); }
private void submitEdit(double x, double y, int action) { private void submitEdit(double x, double y, int action) {
switch (input.activeLayer) { switch (input.activeLayer) {
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action)); case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
@@ -7992,6 +8000,11 @@ public class EditorApp extends Application {
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0)); input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_LOCATION_ZONES -> case SharedInput.LAYER_LOCATION_ZONES ->
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0)); input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_ZONEN -> {
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0));
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
}
case SharedInput.LAYER_VOXEL_CLIFF -> case SharedInput.LAYER_VOXEL_CLIFF ->
input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0)); input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_PLAY_TOOL -> { case SharedInput.LAYER_PLAY_TOOL -> {
@@ -8008,6 +8021,7 @@ public class EditorApp extends Application {
new SharedInput.ModelInteractableClick((float) x, (float) y)); new SharedInput.ModelInteractableClick((float) x, (float) y));
} }
} }
scheduleAutoSave();
} }
// ── Statusleiste ───────────────────────────────────────────────────────── // ── Statusleiste ─────────────────────────────────────────────────────────
@@ -8328,6 +8342,7 @@ public class EditorApp extends Application {
if (pressed && (input.activeLayer == SharedInput.LAYER_SOUND_AREAS if (pressed && (input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS || input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_WATER || input.activeLayer == SharedInput.LAYER_WATER
|| input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)) || input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF))
input.cancelZoneDrawing = true; input.cancelZoneDrawing = true;
@@ -8337,12 +8352,17 @@ public class EditorApp extends Application {
if (input.activeLayer == SharedInput.LAYER_OBJECTS if (input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT) || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
input.deleteSelectedRequested = true; input.deleteSelectedRequested = true;
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
input.deleteSoundAreaRequested = true; input.deleteSoundAreaRequested = true; scheduleAutoSave();
else if (input.activeLayer == SharedInput.LAYER_AREAS) } else if (input.activeLayer == SharedInput.LAYER_AREAS) {
input.deleteAreaRequested = true; input.deleteAreaRequested = true; scheduleAutoSave();
else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) } else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) {
input.deleteLocationZoneRequested = true; input.deleteLocationZoneRequested = true; scheduleAutoSave();
} else if (input.activeLayer == SharedInput.LAYER_ZONEN) {
if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); }
else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
}
else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)
input.deleteVoxelCliffZoneRequested = true; input.deleteVoxelCliffZoneRequested = true;
else if (input.activeLayer == SharedInput.LAYER_WATER) else if (input.activeLayer == SharedInput.LAYER_WATER)
@@ -8509,6 +8529,115 @@ public class EditorApp extends Application {
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false); if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false);
} }
// ── Zonen-Panel (vereint Sound, Area, Location) ───────────────────────────
private VBox buildZonenPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
final String STYLE_AREA_ON = "-fx-background-color: #e05050; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
final String STYLE_AREA_OFF = "-fx-background-color: #b03030; -fx-text-fill: #ddd; -fx-font-weight: bold;";
final String STYLE_SOUND_ON = "-fx-background-color: #e07818; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
final String STYLE_SOUND_OFF = "-fx-background-color: #a05010; -fx-text-fill: #ddd; -fx-font-weight: bold;";
final String STYLE_LOC_ON = "-fx-background-color: #c8a800; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
final String STYLE_LOC_OFF = "-fx-background-color: #8a7200; -fx-text-fill: #ddd; -fx-font-weight: bold;";
Button areaKindBtn = new Button("+ Neue Area zeichnen");
Button soundKindBtn = new Button("+ Neuen Sound zeichnen");
Button locationKindBtn = new Button("+ Neue Location zeichnen");
areaKindBtn.setMaxWidth(Double.MAX_VALUE);
soundKindBtn.setMaxWidth(Double.MAX_VALUE);
locationKindBtn.setMaxWidth(Double.MAX_VALUE);
Runnable updateKindStyles = () -> {
String k = input.zonenNewKind; // null = kein Zeichenmodus aktiv
areaKindBtn.setStyle("area".equals(k) ? STYLE_AREA_ON : STYLE_AREA_OFF);
soundKindBtn.setStyle("sound".equals(k) ? STYLE_SOUND_ON : STYLE_SOUND_OFF);
locationKindBtn.setStyle("location".equals(k) ? STYLE_LOC_ON : STYLE_LOC_OFF);
};
updateKindStyles.run();
zonenKindStyleUpdater = updateKindStyles;
areaKindBtn.setOnAction(e -> {
input.zonenNewKind = "area";
updateKindStyles.run();
});
soundKindBtn.setOnAction(e -> {
input.zonenNewKind = "sound";
updateKindStyles.run();
});
locationKindBtn.setOnAction(e -> {
input.zonenNewKind = "location";
updateKindStyles.run();
});
areaKindBtn.setTooltip(new javafx.scene.control.Tooltip(
"Area: Musik-Zone mit Tag-, Nacht- und Kampf-Track.\nAreas dürfen sich nicht überschneiden."));
soundKindBtn.setTooltip(new javafx.scene.control.Tooltip(
"Sound: Ambient-Soundbereich mit wählbarer Audio-Datei,\nLautstärke und Loop-Crossfade."));
locationKindBtn.setTooltip(new javafx.scene.control.Tooltip(
"Location: Gameplay-Trigger-Zone, z.B. zum Starten\nvon Quests oder Dialogen beim Betreten."));
inner.getChildren().addAll(
sectionTitle("Neue Zone"),
areaKindBtn, soundKindBtn, locationKindBtn,
new Separator(),
styledHint("L-Klick → Polygon-Punkte setzen"),
styledHint("R-Klick (beim Zeichnen) → letzten Punkt rückgängig"),
styledHint("R-Klick (sonst) → Auswahl aufheben"),
styledHint("ESC → Zeichnen abbrechen"),
styledHint("Entf → Gewählte Zone löschen"),
new Separator(),
sectionTitle("Gewählte Zone"),
new Separator());
zonenDynamicContent = new VBox(6);
Label noSel = new Label("Keine Zone ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
zonenDynamicContent.getChildren().add(noSel);
inner.getChildren().add(zonenDynamicContent);
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;
}
private void updateZonenPanel(String kind) {
if (zonenDynamicContent == null) return;
zonenDynamicContent.getChildren().clear();
switch (kind != null ? kind : "") {
case "area" -> {
VBox saved = areaDynamicContent;
areaDynamicContent = zonenDynamicContent;
updateAreaPanel(input.selectedAreaInfo);
areaDynamicContent = saved;
}
case "sound" -> {
VBox saved = soundAreaDynamicContent;
soundAreaDynamicContent = zonenDynamicContent;
updateSoundAreaPanel(input.selectedSoundAreaInfo);
soundAreaDynamicContent = saved;
}
case "location" -> {
VBox saved = locationZoneDynamicContent;
locationZoneDynamicContent = zonenDynamicContent;
updateLocationZonePanel(input.selectedLocationZoneInfo);
locationZoneDynamicContent = saved;
}
default -> {
Label noSel = new Label("Keine Zone ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
zonenDynamicContent.getChildren().add(noSel);
}
}
}
private VBox buildSoundAreaPanel() { private VBox buildSoundAreaPanel() {
VBox inner = new VBox(8); VBox inner = new VBox(8);
inner.setPadding(new Insets(10)); inner.setPadding(new Insets(10));
@@ -8575,27 +8704,17 @@ public class EditorApp extends Application {
soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
soundLabel.setWrapText(true); soundLabel.setWrapText(true);
Button browseBtn = new Button("📂 Datei wählen…"); Button browseBtn = new Button("🔊 Sound wählen…");
browseBtn.setMaxWidth(Double.MAX_VALUE); browseBtn.setMaxWidth(Double.MAX_VALUE);
browseBtn.setOnAction(e -> { browseBtn.setOnAction(e -> {
FileChooser fc = new FileChooser();
fc.setTitle("Sound-Datei wählen");
fc.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources"); Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
if (java.nio.file.Files.isDirectory(assetRoot)) new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL)
fc.setInitialDirectory(assetRoot.toFile()); .showAndWait()
File chosen = fc.showOpenDialog(primaryStage); .ifPresent(rel -> {
if (chosen != null) { soundPath[0] = rel;
try { soundLabel.setText("Sound: " + rel);
String rel = ensureOgg(chosen, assetRoot); sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
soundPath[0] = rel; });
soundLabel.setText("Sound: " + rel);
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
} catch (Exception ex) {
setStatus("Fehler bei OGG-Konvertierung: " + ex.getMessage());
}
}
}); });
Label volLbl = new Label("Lautstärke"); Label volLbl = new Label("Lautstärke");
@@ -8639,6 +8758,7 @@ public class EditorApp extends Application {
// pendingSoundArea != null and idx matches to update only the props. // pendingSoundArea != null and idx matches to update only the props.
input.pendingSoundArea.set(new de.blight.common.PlacedSoundArea( input.pendingSoundArea.set(new de.blight.common.PlacedSoundArea(
new float[0], new float[0], soundPath, volume, crossfade)); new float[0], new float[0], soundPath, volume, crossfade));
scheduleAutoSave();
} }
// ── Musik-Bereich-Panel ─────────────────────────────────────────────────── // ── Musik-Bereich-Panel ───────────────────────────────────────────────────
@@ -8647,18 +8767,18 @@ public class EditorApp extends Application {
VBox inner = new VBox(8); VBox inner = new VBox(8);
inner.setPadding(new Insets(10)); inner.setPadding(new Insets(10));
inner.getChildren().addAll( inner.getChildren().addAll(
sectionTitle("Bereiche"), sectionTitle("Areas"),
styledHint("L-Klick → Polygon-Punkte setzen"), styledHint("L-Klick → Polygon-Punkte setzen"),
styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
styledHint("ESC → Zeichnen abbrechen"), styledHint("ESC → Zeichnen abbrechen"),
styledHint("Entf → Bereich löschen"), styledHint("Entf → Area löschen"),
styledHint("Bereiche dürfen sich nicht überschneiden"), styledHint("Areas dürfen sich nicht überschneiden"),
new Separator(), new Separator(),
sectionTitle("Gewählter Bereich"), sectionTitle("Gewählte Area"),
new Separator()); new Separator());
areaDynamicContent = new VBox(6); areaDynamicContent = new VBox(6);
Label noSel = new Label("Kein Bereich ausgewählt"); Label noSel = new Label("Keine Area ausgewählt");
noSel.setStyle("-fx-text-fill: #888;"); noSel.setStyle("-fx-text-fill: #888;");
areaDynamicContent.getChildren().add(noSel); areaDynamicContent.getChildren().add(noSel);
inner.getChildren().add(areaDynamicContent); inner.getChildren().add(areaDynamicContent);
@@ -8679,71 +8799,80 @@ public class EditorApp extends Application {
areaDynamicContent.getChildren().clear(); areaDynamicContent.getChildren().clear();
if (info == null) { if (info == null) {
Label noSel = new Label("Kein Bereich ausgewählt"); Label noSel = new Label("Keine Area ausgewählt");
noSel.setStyle("-fx-text-fill: #888;"); noSel.setStyle("-fx-text-fill: #888;");
areaDynamicContent.getChildren().add(noSel); areaDynamicContent.getChildren().add(noSel);
return; return;
} }
// Format: "idx|nameId|dayTrack|nightTrack|combatTrack" // Format: "idx|areaId|triggersJson"
String[] p = info.split("\\|", -1); String[] p = info.split("\\|", 3);
if (p.length < 5) return; if (p.length < 1) return;
try { try {
int idx = Integer.parseInt(p[0]); int idx = Integer.parseInt(p[0]);
final String[] nameRef = {p[1]}; String currentAreaId = p.length > 1 ? p[1] : "";
final String[] tracks = {p[2], p[3], p[4]}; java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
String[] trackLabels = {"☀ Tag-Track", "🌙 Nacht-Track", "⚔ Kampf-Track"}; p.length > 2
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
: new java.util.ArrayList<>();
Runnable publish = () -> // Verfügbare Area-Definitionen laden
input.pendingArea.set(new de.blight.common.PlacedArea( java.util.List<String> areaIds = new java.util.ArrayList<>();
new float[0], new float[0], nameRef[0], tracks[0], tracks[1], tracks[2])); areaIds.add("");
try {
de.blight.common.AreaDefinitionIO.load().stream()
.map(de.blight.common.AreaDefinition::id)
.filter(s -> !s.isBlank())
.forEach(areaIds::add);
} catch (Exception ignored) {}
// Name (TextReference-ID) Label areaLbl = new Label("Verknüpfte Area:");
Label nameLbl = new Label("Name (TextReference):"); areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
TextField nameTf = new TextField(nameRef[0]);
nameTf.setPromptText("z.B. area.village");
nameTf.textProperty().addListener((obs, o, n) -> { nameRef[0] = n; publish.run(); });
areaDynamicContent.getChildren().addAll(nameLbl, nameTf, new Separator());
for (int ti = 0; ti < 3; ti++) { Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name");
final int tIdx = ti; nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
Label lbl = new Label(trackLabels[ti] + ": "
+ (tracks[ti].isEmpty() ? "(keine)" : tracks[ti]));
lbl.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
lbl.setWrapText(true);
Button btn = new Button("📂 Wählen…"); // editorHolder muss vor areaCombo deklariert sein (Lambda-Forward-Ref)
btn.setMaxWidth(Double.MAX_VALUE); de.blight.editor.ui.TriggerListEditor[] editorHolder = {null};
btn.setOnAction(e -> {
FileChooser fc = new FileChooser();
fc.setTitle(trackLabels[tIdx] + " wählen");
fc.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
if (java.nio.file.Files.isDirectory(assetRoot))
fc.setInitialDirectory(assetRoot.toFile());
File chosen = fc.showOpenDialog(primaryStage);
if (chosen != null) {
try {
Path assetRootPath = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
tracks[tIdx] = ensureOgg(chosen, assetRootPath);
lbl.setText(trackLabels[tIdx] + ": " + tracks[tIdx]);
publish.run();
} catch (Exception ex) {
setStatus("Fehler bei OGG-Konvertierung: " + ex.getMessage());
}
}
});
areaDynamicContent.getChildren().addAll(lbl, btn); ComboBox<String> areaCombo = new ComboBox<>();
if (ti < 2) areaDynamicContent.getChildren().add(new Separator()); areaCombo.getItems().setAll(areaIds);
} areaCombo.setValue(areaIds.contains(currentAreaId) ? currentAreaId : "");
areaCombo.setMaxWidth(Double.MAX_VALUE);
areaCombo.setPromptText("Area wählen…");
areaCombo.valueProperty().addListener((obs, o, n) -> {
String aid = n != null ? n : "";
nameKeyLbl.setText(aid.isBlank() ? "" : aid + ".name");
java.util.List<de.blight.common.model.trigger.Trigger> curTriggers =
editorHolder[0] != null
? new java.util.ArrayList<>(editorHolder[0].getTriggers())
: existingTriggers;
input.pendingArea.set(new de.blight.common.PlacedArea(
new float[0], new float[0], aid, curTriggers));
scheduleAutoSave();
});
Label triggerLbl = new Label("Trigger:");
triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
editorHolder[0] = new de.blight.editor.ui.TriggerListEditor(existingTriggers, () -> {
String aid = areaCombo.getValue() != null ? areaCombo.getValue() : currentAreaId;
input.pendingArea.set(new de.blight.common.PlacedArea(
new float[0], new float[0], aid,
new java.util.ArrayList<>(editorHolder[0].getTriggers())));
scheduleAutoSave();
});
Button delBtn = new Button("🗑 Löschen"); Button delBtn = new Button("🗑 Löschen");
delBtn.setMaxWidth(Double.MAX_VALUE); delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;"); delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> input.deleteAreaRequested = true); delBtn.setOnAction(e -> { input.deleteAreaRequested = true; scheduleAutoSave(); });
areaDynamicContent.getChildren().addAll(new Separator(), delBtn);
areaDynamicContent.getChildren().addAll(
areaLbl, areaCombo, nameKeyLbl,
new Separator(),
triggerLbl, editorHolder[0],
new Separator(),
delBtn);
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
@@ -8795,14 +8924,11 @@ public class EditorApp extends Application {
try { try {
int idx = Integer.parseInt(p[0]); int idx = Integer.parseInt(p[0]);
String nameId = p.length > 1 ? p[1] : ""; String nameId = p.length > 1 ? p[1] : "";
java.util.List<de.blight.common.model.trigger.Trigger> initTriggers = java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
p.length > 2 p.length > 2
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2]) ? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
: new java.util.ArrayList<>(); : new java.util.ArrayList<>();
// Holder damit triggerEditor und nameTf sich gegenseitig referenzieren können
final de.blight.editor.ui.TriggerListEditor[] teHolder = {null};
// Verfügbare Locations aus LocationIO laden // Verfügbare Locations aus LocationIO laden
java.util.List<String> locationIds = new java.util.ArrayList<>(); java.util.List<String> locationIds = new java.util.ArrayList<>();
locationIds.add(""); locationIds.add("");
@@ -8815,44 +8941,30 @@ public class EditorApp extends Application {
Label nameLbl = new Label("Verknüpfte Location:"); Label nameLbl = new Label("Verknüpfte Location:");
nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
Label nameKeyLbl = new Label(nameId.isBlank() ? "" : nameId + ".name");
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
ComboBox<String> locationCombo = new ComboBox<>(); ComboBox<String> locationCombo = new ComboBox<>();
locationCombo.getItems().setAll(locationIds); locationCombo.getItems().setAll(locationIds);
locationCombo.setValue(locationIds.contains(nameId) ? nameId : ""); locationCombo.setValue(locationIds.contains(nameId) ? nameId : "");
locationCombo.setMaxWidth(Double.MAX_VALUE); locationCombo.setMaxWidth(Double.MAX_VALUE);
locationCombo.setPromptText("Location wählen…"); locationCombo.setPromptText("Location wählen…");
// Fallback: Freitext falls Location nicht in Liste
TextField nameTf = new TextField(nameId);
nameTf.setPromptText("oder manuell: location.village");
locationCombo.valueProperty().addListener((obs, o, n) -> { locationCombo.valueProperty().addListener((obs, o, n) -> {
if (n != null && !n.isBlank()) nameTf.setText(n); String lid = n != null ? n : "";
nameKeyLbl.setText(lid.isBlank() ? "" : lid + ".name");
input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone( input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
new float[0], new float[0], new float[0], new float[0], lid, existingTriggers));
n != null && !n.isBlank() ? n : nameTf.getText(), scheduleAutoSave();
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers));
}); });
nameTf.textProperty().addListener((obs, o, n) ->
input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
new float[0], new float[0], n,
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers)));
Label triggerLbl = new Label("Trigger:");
triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
de.blight.editor.ui.TriggerListEditor triggerEditor =
new de.blight.editor.ui.TriggerListEditor(initTriggers, () ->
input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
new float[0], new float[0], nameTf.getText(),
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers)));
teHolder[0] = triggerEditor;
Button delBtn = new Button("🗑 Löschen"); Button delBtn = new Button("🗑 Löschen");
delBtn.setMaxWidth(Double.MAX_VALUE); delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;"); delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> input.deleteLocationZoneRequested = true); delBtn.setOnAction(e -> { input.deleteLocationZoneRequested = true; scheduleAutoSave(); });
locationZoneDynamicContent.getChildren().addAll( locationZoneDynamicContent.getChildren().addAll(
nameLbl, locationCombo, nameTf, nameLbl, locationCombo, nameKeyLbl,
new Separator(),
triggerLbl, triggerEditor,
new Separator(), new Separator(),
delBtn); delBtn);
@@ -11239,6 +11351,20 @@ public class EditorApp extends Application {
root.setRight(null); root.setRight(null);
} }
private void switchToAreaEditor() {
onF5 = null;
currentTool = "areaEditor";
ToolBar tb = new ToolBar();
Button backBtn = new Button("← Welteneditor");
backBtn.setOnAction(e -> switchToWorldEditor());
Label label = new Label("Areas-Manager");
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
topBar.getChildren().set(1, tb);
root.setCenter(new de.blight.editor.ui.AreaEditorView());
root.setRight(null);
}
private void switchToLocalizationEditor() { private void switchToLocalizationEditor() {
onF5 = null; onF5 = null;
currentTool = "localizationEditor"; currentTool = "localizationEditor";

View File

@@ -356,9 +356,11 @@ public class SharedInput {
public volatile float pendingGotoPitch = Float.NaN; public volatile float pendingGotoPitch = Float.NaN;
// ── Reload-Signale nach Löschung aus MapObjectsView ────────────────────── // ── Reload-Signale nach Löschung aus MapObjectsView ──────────────────────
public volatile boolean reloadPlacedModels = false; public volatile boolean reloadPlacedModels = false;
public volatile boolean reloadPlacedItems = false; public volatile boolean reloadPlacedItems = false;
public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen
/** Nur Lichter + Emitter neu laden (kein Zonen-Reload), gesetzt von SceneObjectState. */
public volatile boolean reloadLightsEmitters = false;
/** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */ /** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */
public volatile float camYaw = 0f; public volatile float camYaw = 0f;
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */ /** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
@@ -546,6 +548,18 @@ public class SharedInput {
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */ /** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
public volatile boolean cancelZoneDrawing = false; public volatile boolean cancelZoneDrawing = false;
// ── Zonen-Werkzeug (vereint Sound, Area, Location) ────────────────────────
/** activeLayer==32 → alle Zonen-Typen gleichzeitig sichtbar und editierbar */
public static final int LAYER_ZONEN = 32;
/** JavaFX → JME: welcher Zonen-Typ neu gezeichnet wird ("area"|"sound"|"location"). */
public volatile String zonenNewKind = "area";
/** JME → JavaFX: Typ der aktuell selektierten Zone ("area"|"sound"|"location"|null). */
public volatile String zonenSelectedKind = null;
/** JME → JavaFX: Selektion hat sich geändert → Zonen-Panel aktualisieren. */
public volatile boolean zonenSelectionChanged = false;
// ── Spiel-Starten-Werkzeug ──────────────────────────────────────────────── // ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
/** Klick/Drag-Ereignisse im Viewport für das Play-Tool. */ /** Klick/Drag-Ereignisse im Viewport für das Play-Tool. */
public record PlayToolClick(float screenX, float screenY) {} public record PlayToolClick(float screenX, float screenY) {}

View File

@@ -13,6 +13,7 @@ import com.jme3.scene.VertexBuffer;
import com.jme3.terrain.geomipmap.TerrainQuad; import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.PlacedArea; import de.blight.common.PlacedArea;
import de.blight.common.model.trigger.TriggerIO;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
@@ -23,10 +24,10 @@ public class AreaState extends BaseAppState {
private static final float LINE_OFFSET_Y = 0.35f; private static final float LINE_OFFSET_Y = 0.35f;
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.5f, 0.3f, 1f, 1f); private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.90f, 0.15f, 0.15f, 1f); // Rot
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f); private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1.00f, 0.55f, 0.55f, 1f); // Hell-Rot
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.8f, 0.5f, 1f, 1f); private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.40f, 0.40f, 1f); // Mittel-Rot
private static final ColorRGBA COLOR_OVERLAP = new ColorRGBA(1f, 0.2f, 0.2f, 1f); private static final ColorRGBA COLOR_OVERLAP = new ColorRGBA(1.00f, 0.85f, 0.00f, 1f); // Gelb (Fehler)
private final SharedInput input; private final SharedInput input;
private SimpleApplication app; private SimpleApplication app;
@@ -79,7 +80,7 @@ public class AreaState extends BaseAppState {
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_AREAS) { if (input.activeLayer != SharedInput.LAYER_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN) {
if (placing) cancelPoly(); if (placing) cancelPoly();
return; return;
} }
@@ -94,9 +95,9 @@ public class AreaState extends BaseAppState {
applyProperty(selectedIdx, pending); applyProperty(selectedIdx, pending);
} }
if (input.cancelZoneDrawing) { if (input.cancelZoneDrawing && placing) {
input.cancelZoneDrawing = false; input.cancelZoneDrawing = false;
if (placing) cancelPoly(); cancelPoly();
} }
if (input.deleteAreaRequested) { if (input.deleteAreaRequested) {
@@ -116,16 +117,13 @@ public class AreaState extends BaseAppState {
Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
if (click.rightButton()) { if (click.rightButton()) {
if (placing) closePoly(); if (placing) undoLastPoint();
else deselect(); else deselect();
return; return;
} }
if (terrain == null) return; Vector3f pt = raycastAll(ray);
CollisionResults hits = new CollisionResults(); if (pt == null) return;
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
float hitX = pt.x, hitZ = pt.z; float hitX = pt.x, hitZ = pt.z;
if (placing) { if (placing) {
@@ -133,10 +131,11 @@ public class AreaState extends BaseAppState {
hitX = snapped[0]; hitX = snapped[0];
hitZ = snapped[1]; hitZ = snapped[1];
// auto-close only when snapped exactly to own first vertex
if (currX.size() >= 3) { if (currX.size() >= 3) {
float dx = hitX - currX.get(0); float dx = hitX - currX.get(0);
float dz = hitZ - currZ.get(0); float dz = hitZ - currZ.get(0);
if (dx * dx + dz * dz < SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST * 0.25f) { if (dx * dx + dz * dz < 0.01f) {
closePoly(); closePoly();
return; return;
} }
@@ -146,19 +145,41 @@ public class AreaState extends BaseAppState {
currZ.add(hitZ); currZ.add(hitZ);
updateInProgressGeo(); updateInProgressGeo();
} else { } else {
// don't select while another state is drawing
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
if ((sas0 != null && sas0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
// pass 1: clearly inside
for (int i = 0; i < areas.size(); i++) { for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i); PlacedArea a = areas.get(i);
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) { if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
selectArea(i); selectArea(i); return;
return;
} }
} }
// pass 2: within 50 cm of any edge
for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i);
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
selectArea(i); return;
}
}
// cross-type: if another zone type is here, don't start drawing
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null && sas.findZoneAt(hitX, hitZ) >= 0) return;
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null && lzs.findZoneAt(hitX, hitZ) >= 0) return;
}
deselect(); deselect();
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"area".equals(input.zonenNewKind)) return;
placing = true; placing = true;
currX.clear(); currX.clear();
currZ.clear(); currZ.clear();
currX.add(hitX); // snap first point to nearby existing vertices
currZ.add(hitZ); float[] startSnap = snapVertex(hitX, hitZ);
currX.add(startSnap[0]);
currZ.add(startSnap[1]);
updateInProgressGeo(); updateInProgressGeo();
} }
} }
@@ -195,9 +216,10 @@ public class AreaState extends BaseAppState {
return; return;
} }
PlacedArea area = new PlacedArea(xs, zs, "", "", "", ""); PlacedArea area = new PlacedArea(xs, zs, "");
addArea(area); addArea(area);
selectArea(areas.size() - 1); selectArea(areas.size() - 1);
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
cancelPoly(); cancelPoly();
} }
@@ -209,6 +231,14 @@ public class AreaState extends BaseAppState {
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; } if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
} }
private void undoLastPoint() {
if (currX.isEmpty()) { cancelPoly(); return; }
currX.remove(currX.size() - 1);
currZ.remove(currZ.size() - 1);
if (currX.isEmpty()) cancelPoly();
else updateInProgressGeo();
}
private void updateInProgressGeo() { private void updateInProgressGeo() {
if (inProgGeo != null) rootNode.detachChild(inProgGeo); if (inProgGeo != null) rootNode.detachChild(inProgGeo);
int n = currX.size(); int n = currX.size();
@@ -224,7 +254,7 @@ public class AreaState extends BaseAppState {
float x = currX.get(currX.size() - 1); float x = currX.get(currX.size() - 1);
float z = currZ.get(currZ.size() - 1); float z = currZ.get(currZ.size() - 1);
float y = (terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f) + LINE_OFFSET_Y + 0.05f; float y = getHeightAt(x, z) + LINE_OFFSET_Y + 0.05f;
float s = 1.5f; float s = 1.5f;
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3); FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
@@ -251,25 +281,42 @@ public class AreaState extends BaseAppState {
private void selectArea(int idx) { private void selectArea(int idx) {
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) { if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL); areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
} }
// cross-state deselect
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deselectSilent();
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deselectSilent();
selectedIdx = idx; selectedIdx = idx;
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED); areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
areaGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(8f);
publishSelection(idx); publishSelection(idx);
} }
private void deselect() { private void deselect() {
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) { if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL); areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
} }
selectedIdx = -1; selectedIdx = -1;
input.selectedAreaInfo = null; input.selectedAreaInfo = null;
input.areaSelectionChanged = true; input.areaSelectionChanged = true;
if ("area".equals(input.zonenSelectedKind)) {
input.zonenSelectedKind = null;
input.zonenSelectionChanged = true;
}
} }
private void publishSelection(int idx) { private void publishSelection(int idx) {
PlacedArea a = areas.get(idx); PlacedArea a = areas.get(idx);
input.selectedAreaInfo = idx + "|" + a.nameId() + "|" + a.dayTrack() + "|" + a.nightTrack() + "|" + a.combatTrack(); String triggersJson = TriggerIO.serializeList(a.triggers());
input.selectedAreaInfo = idx + "|" + a.areaId() + "|" + triggersJson;
input.areaSelectionChanged = true; input.areaSelectionChanged = true;
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
input.zonenSelectedKind = "area";
input.zonenSelectionChanged = true;
}
} }
// ── Add / Remove / Apply ────────────────────────────────────────────────── // ── Add / Remove / Apply ──────────────────────────────────────────────────
@@ -303,9 +350,9 @@ public class AreaState extends BaseAppState {
private void applyProperty(int idx, PlacedArea updated) { private void applyProperty(int idx, PlacedArea updated) {
if (updated.pointsX().length == 0) { if (updated.pointsX().length == 0) {
PlacedArea existing = areas.get(idx); PlacedArea existing = areas.get(idx);
areas.set(idx, new PlacedArea( areas.set(idx, new PlacedArea(existing.pointsX(), existing.pointsZ(),
existing.pointsX(), existing.pointsZ(), updated.areaId(),
updated.nameId(), updated.dayTrack(), updated.nightTrack(), updated.combatTrack())); updated.triggers() != null ? updated.triggers() : existing.triggers()));
} else { } else {
areas.set(idx, updated); areas.set(idx, updated);
} }
@@ -317,7 +364,7 @@ public class AreaState extends BaseAppState {
int n = xs.size(); int n = xs.size();
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3); FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
float hy = terrain != null ? terrain.getHeight(new Vector2f(xs.get(i), zs.get(i))) : 0f; float hy = getHeightAt(xs.get(i), zs.get(i));
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i)); posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
} }
posBuffer.flip(); posBuffer.flip();
@@ -329,13 +376,29 @@ public class AreaState extends BaseAppState {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color); mat.setColor("Color", color);
mat.getAdditionalRenderState().setLineWidth(2f); mat.getAdditionalRenderState().setLineWidth(4f);
Geometry geo = new Geometry(name, mesh); Geometry geo = new Geometry(name, mesh);
geo.setMaterial(mat); geo.setMaterial(mat);
return geo; return geo;
} }
public boolean isPlacing() { return placing; }
void deselectSilent() {
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
}
selectedIdx = -1;
input.selectedAreaInfo = null;
input.areaSelectionChanged = true;
if ("area".equals(input.zonenSelectedKind)) {
input.zonenSelectedKind = null;
input.zonenSelectionChanged = true;
}
}
// ── Overlap detection ───────────────────────────────────────────────────── // ── Overlap detection ─────────────────────────────────────────────────────
private boolean overlapsExistingAreas(float[] xs, float[] zs) { private boolean overlapsExistingAreas(float[] xs, float[] zs) {
@@ -383,6 +446,15 @@ public class AreaState extends BaseAppState {
return new ArrayList<>(areas); return new ArrayList<>(areas);
} }
public int findZoneAt(float wx, float wz) {
for (int i = 0; i < areas.size(); i++) {
PlacedArea a = areas.get(i);
if (SoundAreaState.pointInPolygon(wx, wz, a.pointsX(), a.pointsZ())
|| SoundAreaState.pointNearPolygonEdge(wx, wz, a.pointsX(), a.pointsZ(), 0.5f)) return i;
}
return -1;
}
public void loadAreas(List<PlacedArea> loaded) { public void loadAreas(List<PlacedArea> loaded) {
if (rootNode == null) { if (rootNode == null) {
pendingAreas = new ArrayList<>(loaded); pendingAreas = new ArrayList<>(loaded);
@@ -405,4 +477,66 @@ public class AreaState extends BaseAppState {
for (float f : arr) l.add(f); for (float f : arr) l.add(f);
return l; return l;
} }
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
best = hits.getClosestCollision().getContactPoint();
bestDistSq = ray.getOrigin().distanceSquared(best);
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
private float getHeightAt(float wx, float wz) {
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
float best = 0f;
float bestDistFromTop = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res);
if (res.size() > 0) {
float y = res.getClosestCollision().getContactPoint().y;
float d = 9999f - y;
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = 9999f - vp.y;
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = 9999f - sp.y;
if (d < bestDistFromTop) { best = sp.y; }
}
}
return best;
}
} }

View File

@@ -119,11 +119,8 @@ public class EmitterState extends BaseAppState {
if (click.rightButton()) { deselect(); return; } if (click.rightButton()) { deselect(); return; }
if (terrain == null) return; Vector3f pt = raycastAll(ray);
CollisionResults hits = new CollisionResults(); if (pt == null) return;
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
PlacedEmitter pe = createPreset(input.emitterPreset, pt.x, pt.y, pt.z); PlacedEmitter pe = createPreset(input.emitterPreset, pt.x, pt.y, pt.z);
addEmitter(pe); addEmitter(pe);
@@ -327,4 +324,34 @@ public class EmitterState extends BaseAppState {
clearAll(); clearAll();
for (PlacedEmitter pe : loaded) addEmitter(pe); for (PlacedEmitter pe : loaded) addEmitter(pe);
} }
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
best = hits.getClosestCollision().getContactPoint();
bestDistSq = ray.getOrigin().distanceSquared(best);
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
} }

View File

@@ -129,12 +129,8 @@ public class LightState extends BaseAppState {
return; return;
} }
// Neues Licht auf dem Terrain platzieren Vector3f pt = raycastAll(ray);
if (terrain == null) return; if (pt == null) return;
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
PlacedLight pl = new PlacedLight(pt.x, pt.y + 1f, pt.z, 1f, 1f, 1f, 1f, 20f); PlacedLight pl = new PlacedLight(pt.x, pt.y + 1f, pt.z, 1f, 1f, 1f, 1f, 20f);
addLight(pl); addLight(pl);
@@ -317,4 +313,34 @@ public class LightState extends BaseAppState {
addLight(pl); addLight(pl);
} }
} }
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
best = hits.getClosestCollision().getContactPoint();
bestDistSq = ray.getOrigin().distanceSquared(best);
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
} }

View File

@@ -23,9 +23,9 @@ public class LocationZoneState extends BaseAppState {
private static final float LINE_OFFSET_Y = 0.40f; private static final float LINE_OFFSET_Y = 0.40f;
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.2f, 0.8f, 0.4f, 1f); private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(1.00f, 0.85f, 0.00f, 1f); // Gelb
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f); private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1.00f, 1.00f, 0.55f, 1f); // Hell-Gelb
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.5f, 1f, 0.6f, 1f); private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.95f, 0.30f, 1f); // Mittel-Gelb
private final SharedInput input; private final SharedInput input;
private SimpleApplication app; private SimpleApplication app;
@@ -78,7 +78,7 @@ public class LocationZoneState extends BaseAppState {
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_LOCATION_ZONES) { if (input.activeLayer != SharedInput.LAYER_LOCATION_ZONES && input.activeLayer != SharedInput.LAYER_ZONEN) {
if (placing) cancelPoly(); if (placing) cancelPoly();
return; return;
} }
@@ -93,9 +93,9 @@ public class LocationZoneState extends BaseAppState {
applyProperty(selectedIdx, pending); applyProperty(selectedIdx, pending);
} }
if (input.cancelZoneDrawing) { if (input.cancelZoneDrawing && placing) {
input.cancelZoneDrawing = false; input.cancelZoneDrawing = false;
if (placing) cancelPoly(); cancelPoly();
} }
if (input.deleteLocationZoneRequested) { if (input.deleteLocationZoneRequested) {
@@ -115,16 +115,13 @@ public class LocationZoneState extends BaseAppState {
Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
if (click.rightButton()) { if (click.rightButton()) {
if (placing) closePoly(); if (placing) undoLastPoint();
else deselect(); else deselect();
return; return;
} }
if (terrain == null) return; Vector3f pt = raycastAll(ray);
CollisionResults hits = new CollisionResults(); if (pt == null) return;
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
float hitX = pt.x, hitZ = pt.z; float hitX = pt.x, hitZ = pt.z;
if (placing) { if (placing) {
@@ -132,10 +129,11 @@ public class LocationZoneState extends BaseAppState {
hitX = snapped[0]; hitX = snapped[0];
hitZ = snapped[1]; hitZ = snapped[1];
// auto-close only when snapped exactly to own first vertex
if (currX.size() >= 3) { if (currX.size() >= 3) {
float dx = hitX - currX.get(0); float dx = hitX - currX.get(0);
float dz = hitZ - currZ.get(0); float dz = hitZ - currZ.get(0);
if (dx * dx + dz * dz < SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST * 0.25f) { if (dx * dx + dz * dz < 0.01f) {
closePoly(); closePoly();
return; return;
} }
@@ -145,18 +143,40 @@ public class LocationZoneState extends BaseAppState {
currZ.add(hitZ); currZ.add(hitZ);
updateInProgressGeo(); updateInProgressGeo();
} else { } else {
// don't select while another state is drawing
AreaState as0 = getStateManager().getState(AreaState.class);
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
if ((as0 != null && as0.isPlacing()) || (sas0 != null && sas0.isPlacing())) return;
// pass 1: clearly inside
for (int i = 0; i < zones.size(); i++) { for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i); PlacedLocationZone z = zones.get(i);
if (SoundAreaState.pointInPolygon(hitX, hitZ, z.pointsX(), z.pointsZ())) { if (SoundAreaState.pointInPolygon(hitX, hitZ, z.pointsX(), z.pointsZ())) {
selectZone(i); selectZone(i); return;
return;
} }
} }
// pass 2: within 50 cm of any edge
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, z.pointsX(), z.pointsZ(), 0.5f)) {
selectZone(i); return;
}
}
// cross-type: if another zone type is here, don't start drawing
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null && as.findZoneAt(hitX, hitZ) >= 0) return;
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null && sas.findZoneAt(hitX, hitZ) >= 0) return;
}
deselect(); deselect();
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"location".equals(input.zonenNewKind)) return;
placing = true; placing = true;
currX.clear(); currX.clear();
currZ.clear(); currZ.clear();
currX.add(hitX); // snap first point to nearby existing vertices
float[] startSnap = snapVertex(hitX, hitZ);
currX.add(startSnap[0]);
currZ.add(hitZ); currZ.add(hitZ);
updateInProgressGeo(); updateInProgressGeo();
} }
@@ -189,6 +209,7 @@ public class LocationZoneState extends BaseAppState {
PlacedLocationZone zone = new PlacedLocationZone(xs, zs, ""); PlacedLocationZone zone = new PlacedLocationZone(xs, zs, "");
addZone(zone); addZone(zone);
selectZone(zones.size() - 1); selectZone(zones.size() - 1);
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
cancelPoly(); cancelPoly();
} }
@@ -200,6 +221,14 @@ public class LocationZoneState extends BaseAppState {
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; } if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
} }
private void undoLastPoint() {
if (currX.isEmpty()) { cancelPoly(); return; }
currX.remove(currX.size() - 1);
currZ.remove(currZ.size() - 1);
if (currX.isEmpty()) cancelPoly();
else updateInProgressGeo();
}
private void updateInProgressGeo() { private void updateInProgressGeo() {
if (inProgGeo != null) rootNode.detachChild(inProgGeo); if (inProgGeo != null) rootNode.detachChild(inProgGeo);
int n = currX.size(); int n = currX.size();
@@ -215,7 +244,7 @@ public class LocationZoneState extends BaseAppState {
float x = currX.get(currX.size() - 1); float x = currX.get(currX.size() - 1);
float z = currZ.get(currZ.size() - 1); float z = currZ.get(currZ.size() - 1);
float y = (terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f) + LINE_OFFSET_Y + 0.05f; float y = getHeightAt(x, z) + LINE_OFFSET_Y + 0.05f;
float s = 1.5f; float s = 1.5f;
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3); FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
@@ -242,19 +271,31 @@ public class LocationZoneState extends BaseAppState {
private void selectZone(int idx) { private void selectZone(int idx) {
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) { if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL); zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
zoneGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(6f);
} }
// cross-state deselect
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deselectSilent();
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deselectSilent();
selectedIdx = idx; selectedIdx = idx;
zoneGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED); zoneGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
zoneGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(10f);
publishSelection(idx); publishSelection(idx);
} }
private void deselect() { private void deselect() {
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) { if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL); zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
zoneGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(6f);
} }
selectedIdx = -1; selectedIdx = -1;
input.selectedLocationZoneInfo = null; input.selectedLocationZoneInfo = null;
input.locationZoneSelectionChanged = true; input.locationZoneSelectionChanged = true;
if ("location".equals(input.zonenSelectedKind)) {
input.zonenSelectedKind = null;
input.zonenSelectionChanged = true;
}
} }
private void publishSelection(int idx) { private void publishSelection(int idx) {
@@ -262,6 +303,10 @@ public class LocationZoneState extends BaseAppState {
String triggersJson = de.blight.common.model.trigger.TriggerIO.serializeList(z.triggers()); String triggersJson = de.blight.common.model.trigger.TriggerIO.serializeList(z.triggers());
input.selectedLocationZoneInfo = idx + "|" + z.nameId() + "|" + triggersJson; input.selectedLocationZoneInfo = idx + "|" + z.nameId() + "|" + triggersJson;
input.locationZoneSelectionChanged = true; input.locationZoneSelectionChanged = true;
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
input.zonenSelectedKind = "location";
input.zonenSelectionChanged = true;
}
} }
// ── Add / Remove / Apply ────────────────────────────────────────────────── // ── Add / Remove / Apply ──────────────────────────────────────────────────
@@ -310,7 +355,7 @@ public class LocationZoneState extends BaseAppState {
int n = xs.size(); int n = xs.size();
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3); FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
float hy = terrain != null ? terrain.getHeight(new Vector2f(xs.get(i), zs.get(i))) : 0f; float hy = getHeightAt(xs.get(i), zs.get(i));
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i)); posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
} }
posBuffer.flip(); posBuffer.flip();
@@ -322,19 +367,44 @@ public class LocationZoneState extends BaseAppState {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color); mat.setColor("Color", color);
mat.getAdditionalRenderState().setLineWidth(2f); mat.getAdditionalRenderState().setLineWidth(6f);
Geometry geo = new Geometry(name, mesh); Geometry geo = new Geometry(name, mesh);
geo.setMaterial(mat); geo.setMaterial(mat);
return geo; return geo;
} }
public boolean isPlacing() { return placing; }
void deselectSilent() {
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
zoneGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(6f);
}
selectedIdx = -1;
input.selectedLocationZoneInfo = null;
input.locationZoneSelectionChanged = true;
if ("location".equals(input.zonenSelectedKind)) {
input.zonenSelectedKind = null;
input.zonenSelectionChanged = true;
}
}
// ── Save / Load ─────────────────────────────────────────────────────────── // ── Save / Load ───────────────────────────────────────────────────────────
public List<PlacedLocationZone> getPlacedZones() { public List<PlacedLocationZone> getPlacedZones() {
return new ArrayList<>(zones); return new ArrayList<>(zones);
} }
public int findZoneAt(float wx, float wz) {
for (int i = 0; i < zones.size(); i++) {
PlacedLocationZone z = zones.get(i);
if (SoundAreaState.pointInPolygon(wx, wz, z.pointsX(), z.pointsZ())
|| SoundAreaState.pointNearPolygonEdge(wx, wz, z.pointsX(), z.pointsZ(), 0.5f)) return i;
}
return -1;
}
public void loadZones(List<PlacedLocationZone> loaded) { public void loadZones(List<PlacedLocationZone> loaded) {
if (rootNode == null) { if (rootNode == null) {
pendingZones = new ArrayList<>(loaded); pendingZones = new ArrayList<>(loaded);
@@ -357,4 +427,66 @@ public class LocationZoneState extends BaseAppState {
for (float f : arr) l.add(f); for (float f : arr) l.add(f);
return l; return l;
} }
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
best = hits.getClosestCollision().getContactPoint();
bestDistSq = ray.getOrigin().distanceSquared(best);
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
private float getHeightAt(float wx, float wz) {
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
float best = 0f;
float bestDistFromTop = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res);
if (res.size() > 0) {
float y = res.getClosestCollision().getContactPoint().y;
float d = 9999f - y;
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = 9999f - vp.y;
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = 9999f - sp.y;
if (d < bestDistFromTop) { best = sp.y; }
}
}
return best;
}
} }

View File

@@ -859,7 +859,7 @@ public class SceneObjectState extends BaseAppState {
al.r(), al.g(), al.b(), al.intensity(), al.radius())); al.r(), al.g(), al.b(), al.intensity(), al.radius()));
} }
de.blight.common.LightIO.save(list); de.blight.common.LightIO.save(list);
input.reloadPlacedOther = true; input.reloadLightsEmitters = true;
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
log.error("[SceneObject] Anhang-Licht speichern fehlgeschlagen: {}", e.getMessage()); log.error("[SceneObject] Anhang-Licht speichern fehlgeschlagen: {}", e.getMessage());
} }
@@ -883,7 +883,7 @@ public class SceneObjectState extends BaseAppState {
list.add(pe); list.add(pe);
} }
de.blight.common.EmitterIO.save(list); de.blight.common.EmitterIO.save(list);
input.reloadPlacedOther = true; input.reloadLightsEmitters = true;
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
log.error("[SceneObject] Anhang-Emitter speichern fehlgeschlagen: {}", e.getMessage()); log.error("[SceneObject] Anhang-Emitter speichern fehlgeschlagen: {}", e.getMessage());
} }

View File

@@ -850,6 +850,7 @@ public class SculptedMeshEditorState extends BaseAppState {
/** Raycast gegen alle gebackenen Voxel-Meshes; gibt den nächsten Treffpunkt oder null zurück. */ /** Raycast gegen alle gebackenen Voxel-Meshes; gibt den nächsten Treffpunkt oder null zurück. */
public com.jme3.math.Vector3f raycastGeometry(com.jme3.math.Ray ray) { public com.jme3.math.Vector3f raycastGeometry(com.jme3.math.Ray ray) {
if (sculptRoot == null) return null;
CollisionResults cr = new CollisionResults(); CollisionResults cr = new CollisionResults();
sculptRoot.collideWith(ray, cr); sculptRoot.collideWith(ray, cr);
return cr.size() > 0 ? cr.getClosestCollision().getContactPoint() : null; return cr.size() > 0 ? cr.getClosestCollision().getContactPoint() : null;

View File

@@ -26,9 +26,9 @@ public class SoundAreaState extends BaseAppState {
static final float SNAP_DIST = 8f; static final float SNAP_DIST = 8f;
private static final float LINE_OFFSET_Y = 0.3f; private static final float LINE_OFFSET_Y = 0.3f;
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.1f, 0.85f, 0.4f, 1f); private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(1.00f, 0.50f, 0.05f, 1f); // Orange
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 1f, 0f, 1f); private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1.00f, 0.75f, 0.45f, 1f); // Hell-Orange
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.3f, 0.9f, 1f, 1f); private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.65f, 0.20f, 1f); // Mittel-Orange
private final SharedInput input; private final SharedInput input;
private SimpleApplication app; private SimpleApplication app;
@@ -82,7 +82,7 @@ public class SoundAreaState extends BaseAppState {
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS) { if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN) {
if (placing) cancelPoly(); if (placing) cancelPoly();
return; return;
} }
@@ -97,9 +97,9 @@ public class SoundAreaState extends BaseAppState {
applyProperty(selectedIdx, pending); applyProperty(selectedIdx, pending);
} }
if (input.cancelZoneDrawing) { if (input.cancelZoneDrawing && placing) {
input.cancelZoneDrawing = false; input.cancelZoneDrawing = false;
if (placing) cancelPoly(); cancelPoly();
} }
if (input.deleteSoundAreaRequested) { if (input.deleteSoundAreaRequested) {
@@ -119,30 +119,25 @@ public class SoundAreaState extends BaseAppState {
Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
if (click.rightButton()) { if (click.rightButton()) {
if (placing) closePoly(); if (placing) undoLastPoint();
else deselect(); else deselect();
return; return;
} }
// get terrain hit Vector3f pt = raycastAll(ray);
if (terrain == null) return; if (pt == null) return;
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
float hitX = pt.x, hitZ = pt.z; float hitX = pt.x, hitZ = pt.z;
if (placing) { if (placing) {
// snap to existing vertex?
float[] snapped = snapVertex(hitX, hitZ); float[] snapped = snapVertex(hitX, hitZ);
hitX = snapped[0]; hitX = snapped[0];
hitZ = snapped[1]; hitZ = snapped[1];
// auto-close: if close to first vertex and ≥3 points // auto-close only when snapped exactly to own first vertex
if (currX.size() >= 3) { if (currX.size() >= 3) {
float dx = hitX - currX.get(0); float dx = hitX - currX.get(0);
float dz = hitZ - currZ.get(0); float dz = hitZ - currZ.get(0);
if (dx * dx + dz * dz < SNAP_DIST * SNAP_DIST * 0.25f) { if (dx * dx + dz * dz < 0.01f) {
closePoly(); closePoly();
return; return;
} }
@@ -152,21 +147,41 @@ public class SoundAreaState extends BaseAppState {
currZ.add(hitZ); currZ.add(hitZ);
updateInProgressGeo(); updateInProgressGeo();
} else { } else {
// try to select existing area // don't select while another state is drawing
AreaState as0 = getStateManager().getState(AreaState.class);
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
if ((as0 != null && as0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
// pass 1: clearly inside
for (int i = 0; i < areas.size(); i++) { for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i); PlacedSoundArea a = areas.get(i);
if (pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) { if (pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
selectArea(i); selectArea(i); return;
return;
} }
} }
// no area hit – start new polygon // pass 2: within 50 cm of any edge
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
if (pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
selectArea(i); return;
}
}
// cross-type: if another zone type is here, don't start drawing
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
AreaState as = getStateManager().getState(AreaState.class);
if (as != null && as.findZoneAt(hitX, hitZ) >= 0) return;
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null && lzs.findZoneAt(hitX, hitZ) >= 0) return;
}
deselect(); deselect();
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"sound".equals(input.zonenNewKind)) return;
placing = true; placing = true;
currX.clear(); currX.clear();
currZ.clear(); currZ.clear();
currX.add(hitX); // snap first point to nearby existing vertices
currZ.add(hitZ); float[] startSnap = snapVertex(hitX, hitZ);
currX.add(startSnap[0]);
currZ.add(startSnap[1]);
updateInProgressGeo(); updateInProgressGeo();
} }
} }
@@ -201,6 +216,7 @@ public class SoundAreaState extends BaseAppState {
PlacedSoundArea area = new PlacedSoundArea(xs, zs, "", 1f, false); PlacedSoundArea area = new PlacedSoundArea(xs, zs, "", 1f, false);
addArea(area); addArea(area);
selectArea(areas.size() - 1); selectArea(areas.size() - 1);
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
cancelPoly(); cancelPoly();
} }
@@ -212,6 +228,14 @@ public class SoundAreaState extends BaseAppState {
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; } if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
} }
private void undoLastPoint() {
if (currX.isEmpty()) { cancelPoly(); return; }
currX.remove(currX.size() - 1);
currZ.remove(currZ.size() - 1);
if (currX.isEmpty()) cancelPoly();
else updateInProgressGeo();
}
// ── In-progress visual ──────────────────────────────────────────────────── // ── In-progress visual ────────────────────────────────────────────────────
private void updateInProgressGeo() { private void updateInProgressGeo() {
@@ -229,7 +253,7 @@ public class SoundAreaState extends BaseAppState {
float x = currX.get(currX.size() - 1); float x = currX.get(currX.size() - 1);
float z = currZ.get(currZ.size() - 1); float z = currZ.get(currZ.size() - 1);
float y = (terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f) + LINE_OFFSET_Y + 0.05f; float y = getHeightAt(x, z) + LINE_OFFSET_Y + 0.05f;
float s = 1.5f; float s = 1.5f;
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3); FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
@@ -256,25 +280,41 @@ public class SoundAreaState extends BaseAppState {
private void selectArea(int idx) { private void selectArea(int idx) {
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) { if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL); areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
} }
// cross-state deselect
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deselectSilent();
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deselectSilent();
selectedIdx = idx; selectedIdx = idx;
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED); areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
areaGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(8f);
publishSelection(idx); publishSelection(idx);
} }
private void deselect() { private void deselect() {
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) { if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL); areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
} }
selectedIdx = -1; selectedIdx = -1;
input.selectedSoundAreaInfo = null; input.selectedSoundAreaInfo = null;
input.soundAreaSelectionChanged = true; input.soundAreaSelectionChanged = true;
if ("sound".equals(input.zonenSelectedKind)) {
input.zonenSelectedKind = null;
input.zonenSelectionChanged = true;
}
} }
private void publishSelection(int idx) { private void publishSelection(int idx) {
PlacedSoundArea a = areas.get(idx); PlacedSoundArea a = areas.get(idx);
input.selectedSoundAreaInfo = idx + "|" + a.soundPath() + "|" + a.volume() + "|" + a.crossfade(); input.selectedSoundAreaInfo = idx + "|" + a.soundPath() + "|" + a.volume() + "|" + a.crossfade();
input.soundAreaSelectionChanged = true; input.soundAreaSelectionChanged = true;
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
input.zonenSelectedKind = "sound";
input.zonenSelectionChanged = true;
}
} }
// ── Add / Remove / Apply ────────────────────────────────────────────────── // ── Add / Remove / Apply ──────────────────────────────────────────────────
@@ -325,7 +365,7 @@ public class SoundAreaState extends BaseAppState {
int n = xs.size(); int n = xs.size();
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3); FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
float hy = terrain != null ? terrain.getHeight(new Vector2f(xs.get(i), zs.get(i))) : 0f; float hy = getHeightAt(xs.get(i), zs.get(i));
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i)); posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
} }
posBuffer.flip(); posBuffer.flip();
@@ -337,19 +377,44 @@ public class SoundAreaState extends BaseAppState {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color); mat.setColor("Color", color);
mat.getAdditionalRenderState().setLineWidth(2f); mat.getAdditionalRenderState().setLineWidth(4f);
Geometry geo = new Geometry(name, mesh); Geometry geo = new Geometry(name, mesh);
geo.setMaterial(mat); geo.setMaterial(mat);
return geo; return geo;
} }
public boolean isPlacing() { return placing; }
void deselectSilent() {
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
}
selectedIdx = -1;
input.selectedSoundAreaInfo = null;
input.soundAreaSelectionChanged = true;
if ("sound".equals(input.zonenSelectedKind)) {
input.zonenSelectedKind = null;
input.zonenSelectionChanged = true;
}
}
// ── Save / Load ─────────────────────────────────────────────────────────── // ── Save / Load ───────────────────────────────────────────────────────────
public List<PlacedSoundArea> getPlacedAreas() { public List<PlacedSoundArea> getPlacedAreas() {
return new ArrayList<>(areas); return new ArrayList<>(areas);
} }
public int findZoneAt(float wx, float wz) {
for (int i = 0; i < areas.size(); i++) {
PlacedSoundArea a = areas.get(i);
if (pointInPolygon(wx, wz, a.pointsX(), a.pointsZ())
|| pointNearPolygonEdge(wx, wz, a.pointsX(), a.pointsZ(), 0.5f)) return i;
}
return -1;
}
public void loadAreas(List<PlacedSoundArea> loaded) { public void loadAreas(List<PlacedSoundArea> loaded) {
if (rootNode == null) { if (rootNode == null) {
pendingAreas = new ArrayList<>(loaded); pendingAreas = new ArrayList<>(loaded);
@@ -359,7 +424,28 @@ public class SoundAreaState extends BaseAppState {
for (PlacedSoundArea a : loaded) addArea(a); for (PlacedSoundArea a : loaded) addArea(a);
} }
// ── Point-in-polygon (ray casting) ──────────────────────────────────────── // ── Point-in-polygon + edge proximity ────────────────────────────────────
static boolean pointNearPolygonEdge(float px, float pz, float[] xs, float[] zs, float threshold) {
int n = xs.length;
float t2 = threshold * threshold;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
float ax = xs[i], az = zs[i], bx = xs[j], bz = zs[j];
float dx = bx - ax, dz = bz - az;
float lenSq = dx * dx + dz * dz;
float t;
if (lenSq < 0.0001f) {
t = 0f;
} else {
t = ((px - ax) * dx + (pz - az) * dz) / lenSq;
t = Math.max(0f, Math.min(1f, t));
}
float nx = ax + t * dx - px, nz = az + t * dz - pz;
if (nx * nx + nz * nz < t2) return true;
}
return false;
}
static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) { static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) {
int n = xs.length; int n = xs.length;
@@ -387,4 +473,66 @@ public class SoundAreaState extends BaseAppState {
for (float f : arr) l.add(f); for (float f : arr) l.add(f);
return l; return l;
} }
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
best = hits.getClosestCollision().getContactPoint();
bestDistSq = ray.getOrigin().distanceSquared(best);
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
private float getHeightAt(float wx, float wz) {
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
float best = 0f;
float bestDistFromTop = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res);
if (res.size() > 0) {
float y = res.getClosestCollision().getContactPoint().y;
float d = 9999f - y;
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = 9999f - vp.y;
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = 9999f - sp.y;
if (d < bestDistFromTop) { best = sp.y; }
}
}
return best;
}
} }

View File

@@ -195,9 +195,8 @@ public class StoneEditorState extends BaseAppState {
} }
private void placePendingStoneAt(float wx, float wz) { private void placePendingStoneAt(float wx, float wz) {
if (terrain == null || pendingStone == null) { return; } if (pendingStone == null) { return; }
float th = terrain.getHeight(new Vector2f(wx, wz)); if (!Float.isFinite(getHeightAt(wx, wz))) { return; }
if (!Float.isFinite(th)) { return; }
PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(), PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(),
pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed()); pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed());
int ci = chunkIndex(wx, wz); int ci = chunkIndex(wx, wz);
@@ -247,8 +246,7 @@ public class StoneEditorState extends BaseAppState {
float sx = wx + (float)(Math.cos(angle) * dist); float sx = wx + (float)(Math.cos(angle) * dist);
float sz = wz + (float)(Math.sin(angle) * dist); float sz = wz + (float)(Math.sin(angle) * dist);
float th = terrain.getHeight(new Vector2f(sx, sz)); if (!Float.isFinite(getHeightAt(sx, sz))) continue;
if (!Float.isFinite(th)) continue;
float radius = (float)(minR + random.nextDouble() * (maxR - minR)); float radius = (float)(minR + random.nextDouble() * (maxR - minR));
float rotY = random.nextFloat() * 360f; float rotY = random.nextFloat() * 360f;
@@ -605,12 +603,71 @@ public class StoneEditorState extends BaseAppState {
private Vector3f raycastTerrain(float sx, float sy) { private Vector3f raycastTerrain(float sx, float sy) {
if (terrain == null) return null; if (terrain == null) return null;
Ray ray = new Ray(cam.getWorldCoordinates(new Vector2f(sx, sy), 0f), Vector3f origin = cam.getWorldCoordinates(new Vector2f(sx, sy), 0f);
cam.getWorldCoordinates(new Vector2f(sx, sy), 1f)); Vector3f target = cam.getWorldCoordinates(new Vector2f(sx, sy), 1f);
ray.getDirection().subtractLocal(ray.getOrigin()).normalizeLocal(); Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal());
CollisionResults res = new CollisionResults(); CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res); terrain.collideWith(ray, res);
return res.size() > 0 ? res.getClosestCollision().getContactPoint() : null; Vector3f best = res.size() > 0 ? res.getClosestCollision().getContactPoint() : null;
float bestDistSq = best != null ? origin.distanceSquared(best) : Float.MAX_VALUE;
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = origin.distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = origin.distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
/** Höhe an (wx, wz): nimmt das höchste Ergebnis aus Terrain, Voxeln und Sculpted Mesh. */
private float getHeightAt(float wx, float wz) {
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
float best = Float.NaN;
float bestDistFromTop = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res);
if (res.size() > 0) {
float y = res.getClosestCollision().getContactPoint().y;
float d = 9999f - y;
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = 9999f - vp.y;
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = 9999f - sp.y;
if (d < bestDistFromTop) { best = sp.y; }
}
}
return best;
} }
// ── Hilfsmethoden ──────────────────────────────────────────────────────── // ── Hilfsmethoden ────────────────────────────────────────────────────────
@@ -631,10 +688,8 @@ public class StoneEditorState extends BaseAppState {
} }
private float stoneWorldY(PlacedStone s) { private float stoneWorldY(PlacedStone s) {
if (terrain == null) return 0f; float h = getHeightAt(s.x(), s.z());
float h = terrain.getHeight(new Vector2f(s.x(), s.z())); return Float.isFinite(h) ? h : 0f;
if (!Float.isFinite(h)) return 0f;
return h < -1e10f ? 0f : h;
} }
// ── Persistenz ─────────────────────────────────────────────────────────── // ── Persistenz ───────────────────────────────────────────────────────────

View File

@@ -1062,6 +1062,12 @@ public class TerrainEditorState extends BaseAppState {
terrain.setMaterial(terrainMat = buildTerrainMaterial()); terrain.setMaterial(terrainMat = buildTerrainMaterial());
} }
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) {}
}
if (input.reloadPlacedOther) { if (input.reloadPlacedOther) {
input.reloadPlacedOther = false; input.reloadPlacedOther = false;
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {} try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}

View File

@@ -1333,6 +1333,7 @@ public class VoxelEditorState extends BaseAppState {
/** Raycast gegen reine Voxel-Geometrie; gibt nächsten Treffpunkt oder null zurück. */ /** Raycast gegen reine Voxel-Geometrie; gibt nächsten Treffpunkt oder null zurück. */
public Vector3f raycastVoxelGeometry(com.jme3.math.Ray ray) { public Vector3f raycastVoxelGeometry(com.jme3.math.Ray ray) {
if (voxelRoot == null) return null;
CollisionResults results = new CollisionResults(); CollisionResults results = new CollisionResults();
voxelRoot.collideWith(ray, results); voxelRoot.collideWith(ray, results);
if (results.size() == 0) return null; if (results.size() == 0) return null;

View File

@@ -165,11 +165,8 @@ public class WaterBodyState extends BaseAppState {
return; return;
} }
if (terrain == null) return; Vector3f pt = raycastAll(ray);
CollisionResults hits = new CollisionResults(); if (pt == null) return;
terrain.collideWith(ray, hits);
if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint();
float hitX = pt.x, hitZ = pt.z; float hitX = pt.x, hitZ = pt.z;
if (placing) { if (placing) {
@@ -273,15 +270,14 @@ public class WaterBodyState extends BaseAppState {
private void updatePillarGeo() { private void updatePillarGeo() {
if (pillarGeo != null) { rootNode.detachChild(pillarGeo); pillarGeo = null; } if (pillarGeo != null) { rootNode.detachChild(pillarGeo); pillarGeo = null; }
int n = currX.size(); int n = currX.size();
if (n == 0 || terrain == null) return; if (n == 0) return;
FloatBuffer pos = BufferUtils.createFloatBuffer(n * 2 * 3); FloatBuffer pos = BufferUtils.createFloatBuffer(n * 2 * 3);
FloatBuffer col = BufferUtils.createFloatBuffer(n * 2 * 4); FloatBuffer col = BufferUtils.createFloatBuffer(n * 2 * 4);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
float x = currX.get(i), z = currZ.get(i); float x = currX.get(i), z = currZ.get(i);
Float th = terrain.getHeight(new com.jme3.math.Vector2f(x, z)); float ty = getHeightAt(x, z);
float ty = th != null ? th : currentWaterHeight;
float wy = currentWaterHeight; float wy = currentWaterHeight;
ColorRGBA c = (ty < wy) ? PILLAR_SUB : PILLAR_DRY; ColorRGBA c = (ty < wy) ? PILLAR_SUB : PILLAR_DRY;
@@ -307,18 +303,15 @@ public class WaterBodyState extends BaseAppState {
} }
private void updateCursorPillar() { private void updateCursorPillar() {
if (terrain == null || input.mouseScreenX < 0) { removeCursorPillar(); return; } if (input.mouseScreenX < 0) { removeCursorPillar(); return; }
float jmeX = input.mouseScreenX * (float) input.viewportScaleX; float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY; float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults(); Vector3f pt = raycastAll(ray);
terrain.collideWith(ray, hits); if (pt == null) { removeCursorPillar(); return; }
if (hits.size() == 0) { removeCursorPillar(); return; }
Vector3f pt = hits.getClosestCollision().getContactPoint();
float ty = pt.y, wy = currentWaterHeight; float ty = pt.y, wy = currentWaterHeight;
float delta = wy - ty; float delta = wy - ty;
ColorRGBA c = (delta > 0) ? PILLAR_SUB : PILLAR_DRY; ColorRGBA c = (delta > 0) ? PILLAR_SUB : PILLAR_DRY;
@@ -432,16 +425,13 @@ public class WaterBodyState extends BaseAppState {
// ── Height sampling ─────────────────────────────────────────────────────── // ── Height sampling ───────────────────────────────────────────────────────
private float sampleTerrainAtCursor() { private float sampleTerrainAtCursor() {
if (terrain == null) return Float.NaN;
float jmeX = input.mouseScreenX * (float) input.viewportScaleX; float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY; float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
CollisionResults hits = new CollisionResults(); Vector3f pt = raycastAll(ray);
terrain.collideWith(ray, hits); return pt != null ? pt.y : Float.NaN;
if (hits.size() == 0) return Float.NaN;
return hits.getClosestCollision().getContactPoint().y;
} }
// ── Selection ───────────────────────────────────────────────────────────── // ── Selection ─────────────────────────────────────────────────────────────
@@ -639,4 +629,66 @@ public class WaterBodyState extends BaseAppState {
for (float f : arr) l.add(f); for (float f : arr) l.add(f);
return l; return l;
} }
private Vector3f raycastAll(Ray ray) {
Vector3f best = null;
float bestDistSq = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
if (hits.size() > 0) {
best = hits.getClosestCollision().getContactPoint();
bestDistSq = ray.getOrigin().distanceSquared(best);
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = ray.getOrigin().distanceSquared(vp);
if (d < bestDistSq) { best = vp; bestDistSq = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = ray.getOrigin().distanceSquared(sp);
if (d < bestDistSq) { best = sp; }
}
}
return best;
}
private float getHeightAt(float wx, float wz) {
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
float best = 0f;
float bestDistFromTop = Float.MAX_VALUE;
if (terrain != null) {
CollisionResults res = new CollisionResults();
terrain.collideWith(ray, res);
if (res.size() > 0) {
float y = res.getClosestCollision().getContactPoint().y;
float d = 9999f - y;
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
}
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
Vector3f vp = ves.raycastVoxelGeometry(ray);
if (vp != null) {
float d = 9999f - vp.y;
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
}
}
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
if (smes != null) {
Vector3f sp = smes.raycastGeometry(ray);
if (sp != null) {
float d = 9999f - sp.y;
if (d < bestDistFromTop) { best = sp.y; }
}
}
return best;
}
} }

View File

@@ -0,0 +1,289 @@
package de.blight.editor.ui;
import de.blight.common.AreaDefinition;
import de.blight.common.AreaDefinitionIO;
import de.blight.editor.ProjectRoot;
import javafx.animation.PauseTransition;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.util.Duration;
import java.io.IOException;
import java.nio.file.Path;
public class AreaEditorView extends BorderPane {
private static final String PREFIX = "area.";
private static final String[] TRACK_NAMES = {"☀ Tag-Track", "🌙 Nacht-Track", "⚔ Kampf-Track"};
private final ObservableList<AreaDefinition> areas = FXCollections.observableArrayList();
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
private final String[] tracks = {"", "", ""};
private ListView<AreaDefinition> listView;
private Button deleteBtn;
private AreaDefinition current = null;
private boolean reloading = false;
private TextField idField;
private Label nameKeyLabel;
private Label[] trackLabels = new Label[3];
private VBox formContainer;
{ savePause.setOnFinished(e -> persistCurrent()); }
public AreaEditorView() {
setStyle("-fx-background-color: #1e1e2e;");
reload();
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
split.setDividerPositions(0.28);
setCenter(split);
}
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() { savePause.playFromStart(); }
private void persistCurrent() {
if (current == null || reloading) return;
AreaDefinition saved = formToArea();
if (saved.id().isBlank()) return;
int idx = areas.indexOf(current);
if (idx < 0) return;
reloading = true;
areas.set(idx, saved);
current = saved;
reloading = false;
persist();
}
// ── List panel ────────────────────────────────────────────────────────────
private VBox buildListPanel() {
listView = new ListView<>(areas);
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
listView.setCellFactory(lv -> new ListCell<>() {
@Override protected void updateItem(AreaDefinition d, boolean empty) {
super.updateItem(d, empty);
if (empty || d == null) { setText(null); setStyle(""); return; }
String shortId = d.id().startsWith(PREFIX) ? d.id().substring(PREFIX.length()) : d.id();
setText(shortId.isBlank() ? "—" : shortId);
setStyle("-fx-text-fill: #dddddd;"
+ " -fx-border-color: transparent transparent transparent #cc6644;"
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
}
});
listView.getSelectionModel().selectedItemProperty()
.addListener((obs, old, nw) -> onSelected(old, nw));
VBox.setVgrow(listView, Priority.ALWAYS);
Button newBtn = new Button("Neue Area");
newBtn.setMaxWidth(Double.MAX_VALUE);
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
newBtn.setOnAction(e -> createArea());
deleteBtn = new Button("Löschen");
deleteBtn.setMaxWidth(Double.MAX_VALUE);
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
deleteBtn.setDisable(true);
deleteBtn.setOnAction(e -> deleteSelected());
Button refreshBtn = new Button("↺ Neu laden");
refreshBtn.setMaxWidth(Double.MAX_VALUE);
refreshBtn.setOnAction(e -> reload());
HBox listButtons = new HBox(6, newBtn, deleteBtn);
HBox.setHgrow(newBtn, Priority.ALWAYS);
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
listButtons.setPadding(new Insets(6, 8, 6, 8));
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
VBox.setVgrow(listView, Priority.ALWAYS);
panel.setPadding(new Insets(8));
panel.setStyle("-fx-background-color: #1a1a2a;");
return panel;
}
// ── Form panel ────────────────────────────────────────────────────────────
private ScrollPane buildFormPanel() {
formContainer = buildForm();
formContainer.setDisable(true);
ScrollPane scroll = new ScrollPane(formContainer);
scroll.setFitToWidth(true);
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
return scroll;
}
private VBox buildForm() {
VBox form = new VBox(6);
form.setPadding(new Insets(12));
form.setStyle("-fx-background-color: #252535;");
idField = field("z. B. wald");
nameKeyLabel = new Label("Name-Key: —");
nameKeyLabel.setStyle("-fx-text-fill: #aaaaaa; -fx-font-style: italic; -fx-font-size: 11;");
idField.textProperty().addListener((obs, o, n) -> {
String key = n == null || n.isBlank() ? "" : PREFIX + n.trim() + ".name";
nameKeyLabel.setText("Name-Key: " + (key.isBlank() ? "—" : key));
scheduleSave();
});
idField.focusedProperty().addListener((obs, was, is) -> {
if (!is) { savePause.stop(); persistCurrent(); }
});
for (int i = 0; i < 3; i++) {
trackLabels[i] = new Label("(keine)");
trackLabels[i].setStyle("-fx-text-fill: #aaaaaa; -fx-font-size: 11;");
trackLabels[i].setWrapText(true);
}
form.getChildren().addAll(
sectionTitle("Kennung"),
new Separator(),
row("ID:", idField),
nameKeyLabel,
sectionTitle("Musik-Tracks"),
new Separator(),
trackRow(0),
trackRow(1),
trackRow(2)
);
return form;
}
private VBox trackRow(int idx) {
Button btn = new Button("🎵 Wählen…");
btn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddd; -fx-font-size: 11;");
btn.setOnAction(e -> {
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
new SoundChooser(assetRoot, SoundChooser.Mode.MUSIC)
.showAndWait()
.ifPresent(rel -> {
tracks[idx] = rel;
trackLabels[idx].setText(rel.isEmpty() ? "(keine)" : rel);
scheduleSave();
});
});
Button clearBtn = new Button("✕");
clearBtn.setStyle("-fx-background-color: #5a2a2a; -fx-text-fill: #ddd; -fx-font-size: 10;");
clearBtn.setOnAction(e -> {
tracks[idx] = "";
trackLabels[idx].setText("(keine)");
scheduleSave();
});
Label header = new Label(TRACK_NAMES[idx] + ":");
header.setStyle("-fx-text-fill: #88aacc; -fx-font-size: 11;");
HBox btns = new HBox(4, btn, clearBtn);
return new VBox(2, header, trackLabels[idx], btns);
}
// ── Form load / save ──────────────────────────────────────────────────────
private void onSelected(AreaDefinition old, AreaDefinition nw) {
if (reloading) return;
if (old != null) { saveFormToArea(old); persist(); }
current = nw;
deleteBtn.setDisable(nw == null);
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
else { formContainer.setDisable(true); clearForm(); }
}
private void loadForm(AreaDefinition d) {
String shortId = d.id().startsWith(PREFIX) ? d.id().substring(PREFIX.length()) : d.id();
idField.setText(shortId);
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
tracks[0] = d.dayTrack();
tracks[1] = d.nightTrack();
tracks[2] = d.combatTrack();
for (int i = 0; i < 3; i++) {
trackLabels[i].setText(tracks[i].isEmpty() ? "(keine)" : tracks[i]);
}
}
private AreaDefinition formToArea() {
String shortId = idField.getText().trim();
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
return new AreaDefinition(fullId, tracks[0], tracks[1], tracks[2]);
}
private void saveFormToArea(AreaDefinition old) {
int idx = areas.indexOf(old);
if (idx < 0) return;
areas.set(idx, formToArea());
current = areas.get(idx);
}
private void clearForm() {
idField.clear();
if (nameKeyLabel != null) nameKeyLabel.setText("Name-Key: —");
for (int i = 0; i < 3; i++) {
tracks[i] = "";
if (trackLabels[i] != null) trackLabels[i].setText("(keine)");
}
}
// ── List operations ───────────────────────────────────────────────────────
private void createArea() {
AreaDefinition d = new AreaDefinition(PREFIX + "neu_" + System.currentTimeMillis(), "", "", "");
areas.add(d);
listView.getSelectionModel().select(d);
}
private void deleteSelected() {
if (current == null) return;
areas.remove(current);
current = null;
clearForm();
formContainer.setDisable(true);
deleteBtn.setDisable(true);
persist();
reload();
}
private void persist() {
try { AreaDefinitionIO.save(areas); }
catch (IOException e) {
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
}
}
public void reload() {
try { areas.setAll(AreaDefinitionIO.load()); }
catch (IOException e) { areas.clear(); }
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Label sectionTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #cc8866;");
return l;
}
private static HBox row(String labelText, Node control) {
Label lbl = new Label(labelText);
lbl.setMinWidth(50);
lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(control, Priority.ALWAYS);
HBox box = new HBox(8, lbl, control);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
private static TextField field(String prompt) {
TextField tf = new TextField();
tf.setPromptText(prompt);
return tf;
}
}

View File

@@ -0,0 +1,222 @@
package de.blight.editor.ui;
import de.blight.common.model.trigger.*;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Modality;
import java.util.UUID;
public class ConditionDialog extends Dialog<Condition> {
private static final String TYPE_CHAPTER = "Kapitel erreicht";
private static final String TYPE_FIRST_TIME = "Erstes Mal";
private static final String TYPE_QUEST_COMPLETED = "Quest abgeschlossen";
private static final String TYPE_QUEST_ACCEPTED = "Quest angenommen";
private static final String TYPE_QUEST_REJECTED = "Quest abgelehnt";
private static final String TYPE_GAME_VARIABLE = "Spielvariable gesetzt";
private static final String TYPE_FACTION_MEMBER = "Fraktionsmitglied";
private final ComboBox<String> typeCombo = new ComboBox<>();
private final VBox dynamicArea = new VBox(6);
private Spinner<Integer> chapterSpinner;
private TextField zoneIdField;
private TextField questIdField;
private TextField varKeyField, varValueField;
private TextField fractionIdField;
public ConditionDialog() {
this(null);
}
public ConditionDialog(Condition existing) {
setTitle(existing == null ? "Bedingung hinzufügen" : "Bedingung bearbeiten");
initModality(Modality.APPLICATION_MODAL);
initOwner(Dialogs.primaryWindow());
setResizable(true);
typeCombo.getItems().addAll(TYPE_CHAPTER, TYPE_FIRST_TIME,
TYPE_QUEST_COMPLETED, TYPE_QUEST_ACCEPTED, TYPE_QUEST_REJECTED,
TYPE_GAME_VARIABLE, TYPE_FACTION_MEMBER);
typeCombo.setMaxWidth(Double.MAX_VALUE);
typeCombo.setOnAction(e -> rebuildDynamic(typeCombo.getValue()));
VBox content = new VBox(10);
content.setPadding(new Insets(16));
content.setPrefWidth(380);
content.getChildren().addAll(row("Bedingungstyp:", typeCombo), new Separator(), dynamicArea);
getDialogPane().setContent(content);
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
okBtn.setDisable(true);
typeCombo.valueProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null));
setResultConverter(bt -> bt == ButtonType.OK ? buildCondition() : null);
if (existing != null) {
preload(existing);
} else {
typeCombo.setValue(TYPE_CHAPTER);
}
}
// ── Felder aufbauen ───────────────────────────────────────────────────────
private void rebuildDynamic(String type) {
dynamicArea.getChildren().clear();
if (type == null) return;
switch (type) {
case TYPE_CHAPTER -> buildChapterFields();
case TYPE_FIRST_TIME -> buildFirstTimeFields();
case TYPE_QUEST_COMPLETED,
TYPE_QUEST_ACCEPTED,
TYPE_QUEST_REJECTED -> buildQuestFields();
case TYPE_GAME_VARIABLE -> buildVarFields();
case TYPE_FACTION_MEMBER -> buildFractionFields();
}
}
private void buildChapterFields() {
chapterSpinner = new Spinner<>(0, 99, 0);
chapterSpinner.setEditable(true);
chapterSpinner.setPrefWidth(80);
dynamicArea.getChildren().add(row("Mindest-Kapitel:", chapterSpinner));
}
private void buildFirstTimeFields() {
zoneIdField = field("Zone-ID (nameId der Zone)");
dynamicArea.getChildren().add(row("Zone-ID:", zoneIdField));
}
private void buildQuestFields() {
questIdField = field("Quest-ID (z. B. q_main_001)");
dynamicArea.getChildren().add(row("Quest-ID:", questIdField));
}
private void buildVarFields() {
varKeyField = field("Variablenname");
varValueField = field("Erwarteter Wert (leer = nur Existenz prüfen)");
dynamicArea.getChildren().addAll(
row("Schlüssel:", varKeyField),
row("Wert:", varValueField));
}
private void buildFractionFields() {
fractionIdField = field("UUID der Fraktion");
dynamicArea.getChildren().add(row("Fraktions-UUID:", fractionIdField));
}
// ── Bedingung bauen ───────────────────────────────────────────────────────
private Condition buildCondition() {
String type = typeCombo.getValue();
if (type == null) return null;
return switch (type) {
case TYPE_CHAPTER -> {
ChapterCondition c = new ChapterCondition();
if (chapterSpinner != null) c.setMinChapter(chapterSpinner.getValue());
yield c;
}
case TYPE_FIRST_TIME -> {
FirstTimeCondition c = new FirstTimeCondition();
if (zoneIdField != null) c.setZoneId(zoneIdField.getText().trim());
yield c;
}
case TYPE_QUEST_COMPLETED -> {
QuestCompletedCondition c = new QuestCompletedCondition();
if (questIdField != null) c.setQuestId(questIdField.getText().trim());
yield c;
}
case TYPE_QUEST_ACCEPTED -> {
QuestAcceptedCondition c = new QuestAcceptedCondition();
if (questIdField != null) c.setQuestId(questIdField.getText().trim());
yield c;
}
case TYPE_QUEST_REJECTED -> {
QuestRejectedCondition c = new QuestRejectedCondition();
if (questIdField != null) c.setQuestId(questIdField.getText().trim());
yield c;
}
case TYPE_GAME_VARIABLE -> {
GameVariableCondition c = new GameVariableCondition();
if (varKeyField != null) c.setKey(varKeyField.getText().trim());
if (varValueField != null) c.setValue(varValueField.getText().trim());
yield c;
}
case TYPE_FACTION_MEMBER -> {
FactionMemberCondition c = new FactionMemberCondition();
if (fractionIdField != null && !fractionIdField.getText().isBlank()) {
try { c.setFractionId(UUID.fromString(fractionIdField.getText().trim())); }
catch (IllegalArgumentException ignored) {}
}
yield c;
}
default -> null;
};
}
// ── Vorladen ──────────────────────────────────────────────────────────────
private void preload(Condition c) {
if (c instanceof ChapterCondition cc) {
typeCombo.setValue(TYPE_CHAPTER);
if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(cc.getMinChapter());
} else if (c instanceof FirstTimeCondition ft) {
typeCombo.setValue(TYPE_FIRST_TIME);
if (zoneIdField != null && ft.getZoneId() != null) zoneIdField.setText(ft.getZoneId());
} else if (c instanceof QuestCompletedCondition qc) {
typeCombo.setValue(TYPE_QUEST_COMPLETED);
if (questIdField != null && qc.getQuestId() != null) questIdField.setText(qc.getQuestId());
} else if (c instanceof QuestAcceptedCondition qa) {
typeCombo.setValue(TYPE_QUEST_ACCEPTED);
if (questIdField != null && qa.getQuestId() != null) questIdField.setText(qa.getQuestId());
} else if (c instanceof QuestRejectedCondition qr) {
typeCombo.setValue(TYPE_QUEST_REJECTED);
if (questIdField != null && qr.getQuestId() != null) questIdField.setText(qr.getQuestId());
} else if (c instanceof GameVariableCondition gv) {
typeCombo.setValue(TYPE_GAME_VARIABLE);
if (varKeyField != null && gv.getKey() != null) varKeyField.setText(gv.getKey());
if (varValueField != null && gv.getValue() != null) varValueField.setText(gv.getValue());
} else if (c instanceof FactionMemberCondition fm) {
typeCombo.setValue(TYPE_FACTION_MEMBER);
if (fractionIdField != null && fm.getFractionId() != null)
fractionIdField.setText(fm.getFractionId().toString());
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
static String describe(Condition c) {
if (c instanceof ChapterCondition cc) return "Kapitel ≥ " + cc.getMinChapter();
if (c instanceof FirstTimeCondition ft) return "Erstes Mal: " + nullSafe(ft.getZoneId());
if (c instanceof QuestCompletedCondition q) return "Quest abgeschlossen: " + nullSafe(q.getQuestId());
if (c instanceof QuestAcceptedCondition q) return "Quest angenommen: " + nullSafe(q.getQuestId());
if (c instanceof QuestRejectedCondition q) return "Quest abgelehnt: " + nullSafe(q.getQuestId());
if (c instanceof GameVariableCondition gv) return "Var " + nullSafe(gv.getKey()) + " = " + nullSafe(gv.getValue());
if (c instanceof FactionMemberCondition fm) return "Fraktion: " + (fm.getFractionId() != null ? fm.getFractionId().toString().substring(0, 8) + "…" : "?");
return c.getClass().getSimpleName();
}
private static String nullSafe(String s) { return s != null && !s.isBlank() ? s : "?"; }
private static TextField field(String prompt) {
TextField tf = new TextField();
tf.setPromptText(prompt);
return tf;
}
private static HBox row(String labelText, Node control) {
Label lbl = new Label(labelText);
lbl.setMinWidth(130);
HBox.setHgrow(control, Priority.ALWAYS);
HBox box = new HBox(8, lbl, control);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
}

View File

@@ -1,11 +1,13 @@
package de.blight.editor.ui; package de.blight.editor.ui;
import de.blight.common.model.*; import de.blight.common.model.*;
import javafx.animation.PauseTransition;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -35,6 +37,12 @@ public class CraftingTableEditorView extends BorderPane {
private TextField objectPathField; private TextField objectPathField;
private VBox formContainer; private VBox formContainer;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
{
savePause.setOnFinished(e -> persistCurrent());
}
public CraftingTableEditorView(Path tableDir) { public CraftingTableEditorView(Path tableDir) {
this.tableDir = tableDir; this.tableDir = tableDir;
setStyle("-fx-background-color: #1e1e2e;"); setStyle("-fx-background-color: #1e1e2e;");
@@ -108,14 +116,11 @@ public class CraftingTableEditorView extends BorderPane {
nameIdField = new TextField(); nameIdField = new TextField();
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)"); nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
nameIdField.textProperty().addListener((obs, o, n) -> scheduleSave());
objectPathField = new TextField(); objectPathField = new TextField();
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)"); objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
objectPathField.textProperty().addListener((obs, o, n) -> scheduleSave());
Button saveBtn = new Button("Crafting Table speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrentTable());
form.getChildren().addAll( form.getChildren().addAll(
formTypeLabel, formTypeLabel,
@@ -124,13 +129,22 @@ public class CraftingTableEditorView extends BorderPane {
row("Name-ID:", nameIdField), row("Name-ID:", nameIdField),
new Separator(), new Separator(),
sectionTitle("3D-Objekt"), sectionTitle("3D-Objekt"),
row("Pfad:", objectPathField), row("Pfad:", objectPathField)
new Separator(),
saveBtn
); );
return form; return form;
} }
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() {
savePause.playFromStart();
}
private void persistCurrent() {
if (currentType == null) { return; }
saveCurrentTable();
}
// ── Form load / save ────────────────────────────────────────────────────── // ── Form load / save ──────────────────────────────────────────────────────
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) { private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
@@ -174,7 +188,7 @@ public class CraftingTableEditorView extends BorderPane {
try { try {
CraftingTableIO.save(t, tableDir); CraftingTableIO.save(t, tableDir);
} catch (IOException e) { } catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
return; return;
} }
reloadMap(); reloadMap();
@@ -187,7 +201,7 @@ public class CraftingTableEditorView extends BorderPane {
try { try {
CraftingTableIO.delete(currentType, tableDir); CraftingTableIO.delete(currentType, tableDir);
} catch (IOException e) { } catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
return; return;
} }
reloadMap(); reloadMap();

View File

@@ -696,11 +696,11 @@ public class DialogEditorView extends BorderPane {
} }
String id = result.get().trim(); String id = result.get().trim();
if (id.isBlank()) { if (id.isBlank()) {
new Alert(Alert.AlertType.WARNING, "ID darf nicht leer sein.", ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.WARNING, "ID darf nicht leer sein.", ButtonType.OK).showAndWait();
return; return;
} }
if (allOptions.containsKey(id)) { if (allOptions.containsKey(id)) {
new Alert(Alert.AlertType.WARNING, "ID '" + id + "' bereits vorhanden.", ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.WARNING, "ID '" + id + "' bereits vorhanden.", ButtonType.OK).showAndWait();
return; return;
} }
DialogOption opt = new DialogOption(); DialogOption opt = new DialogOption();
@@ -749,6 +749,7 @@ public class DialogEditorView extends BorderPane {
Dialog<String> dlg = new Dialog<>(); Dialog<String> dlg = new Dialog<>();
dlg.setTitle("Option auswählen"); dlg.setTitle("Option auswählen");
dlg.initModality(Modality.APPLICATION_MODAL); dlg.initModality(Modality.APPLICATION_MODAL);
dlg.initOwner(Dialogs.primaryWindow());
ListView<String> chooser = new ListView<>(); ListView<String> chooser = new ListView<>();
chooser.setCellFactory(lv -> new ListCell<>() { chooser.setCellFactory(lv -> new ListCell<>() {
@@ -794,6 +795,7 @@ public class DialogEditorView extends BorderPane {
Dialog<String> dlg = new Dialog<>(); Dialog<String> dlg = new Dialog<>();
dlg.setTitle("Quest auswählen"); dlg.setTitle("Quest auswählen");
dlg.initModality(Modality.APPLICATION_MODAL); dlg.initModality(Modality.APPLICATION_MODAL);
dlg.initOwner(Dialogs.primaryWindow());
ListView<Quest> chooser = new ListView<>(); ListView<Quest> chooser = new ListView<>();
chooser.setCellFactory(lv -> new ListCell<>() { chooser.setCellFactory(lv -> new ListCell<>() {
@@ -1119,6 +1121,7 @@ public class DialogEditorView extends BorderPane {
private static Trigger buildTriggerDialog(String type) { private static Trigger buildTriggerDialog(String type) {
Dialog<Trigger> dlg = new Dialog<>(); Dialog<Trigger> dlg = new Dialog<>();
dlg.initOwner(Dialogs.primaryWindow());
dlg.setTitle("Trigger konfigurieren: " + type); dlg.setTitle("Trigger konfigurieren: " + type);
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);

View File

@@ -0,0 +1,21 @@
package de.blight.editor.ui;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Window;
public final class Dialogs {
private Dialogs() {}
public static Window primaryWindow() {
return Window.getWindows().stream()
.filter(Window::isShowing)
.findFirst().orElse(null);
}
public static Alert alert(Alert.AlertType type, String msg, ButtonType... btns) {
Alert a = new Alert(type, msg, btns);
a.initOwner(primaryWindow());
return a;
}
}

View File

@@ -1,6 +1,7 @@
package de.blight.editor.ui; package de.blight.editor.ui;
import de.blight.common.model.*; import de.blight.common.model.*;
import javafx.animation.PauseTransition;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
@@ -9,6 +10,7 @@ import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -41,6 +43,12 @@ public class FractionEditorView extends BorderPane {
private TextField rank3Field; private TextField rank3Field;
private VBox formContainer; private VBox formContainer;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
{
savePause.setOnFinished(e -> persistCurrent());
}
public FractionEditorView(Path fractionDir) { public FractionEditorView(Path fractionDir) {
this.fractionDir = fractionDir; this.fractionDir = fractionDir;
this.sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER); this.sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER);
@@ -120,16 +128,17 @@ public class FractionEditorView extends BorderPane {
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;"); idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;");
nameField = field("z. B. faction.guards"); nameField = field("z. B. faction.guards");
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
maleMemberField = field("z. B. faction.guards.member.male"); maleMemberField = field("z. B. faction.guards.member.male");
maleMemberField.textProperty().addListener((obs, o, n) -> scheduleSave());
femaleMemberField = field("z. B. faction.guards.member.female"); femaleMemberField = field("z. B. faction.guards.member.female");
femaleMemberField.textProperty().addListener((obs, o, n) -> scheduleSave());
rank1Field = field("z. B. faction.guards.rank1"); rank1Field = field("z. B. faction.guards.rank1");
rank1Field.textProperty().addListener((obs, o, n) -> scheduleSave());
rank2Field = field("z. B. faction.guards.rank2"); rank2Field = field("z. B. faction.guards.rank2");
rank2Field.textProperty().addListener((obs, o, n) -> scheduleSave());
rank3Field = field("z. B. faction.guards.rank3"); rank3Field = field("z. B. faction.guards.rank3");
rank3Field.textProperty().addListener((obs, o, n) -> scheduleSave());
Button saveBtn = new Button("Fraktion speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrentFraction());
form.getChildren().addAll( form.getChildren().addAll(
sectionTitle("Kennung"), sectionTitle("Kennung"),
@@ -144,17 +153,37 @@ public class FractionEditorView extends BorderPane {
new Separator(), new Separator(),
row("Rang 1:", rank1Field), row("Rang 1:", rank1Field),
row("Rang 2:", rank2Field), row("Rang 2:", rank2Field),
row("Rang 3:", rank3Field), row("Rang 3:", rank3Field)
new Separator(),
saveBtn
); );
return form; return form;
} }
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() {
savePause.playFromStart();
}
private void persistCurrent() {
if (current == null) { return; }
saveFormToFraction(current);
if (current.getFractionId() == null) { return; }
try {
FractionIO.save(current, fractionDir);
} catch (IOException e) {
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
}
}
// ── Form load / save ────────────────────────────────────────────────────── // ── Form load / save ──────────────────────────────────────────────────────
private void onFractionSelected(Fraction old, Fraction nw) { private void onFractionSelected(Fraction old, Fraction nw) {
if (old != null) saveFormToFraction(old); if (old != null) {
saveFormToFraction(old);
if (old.getFractionId() != null) {
try { FractionIO.save(old, fractionDir); } catch (IOException e) { /* ignore on navigation */ }
}
}
current = nw; current = nw;
deleteBtn.setDisable(nw == null); deleteBtn.setDisable(nw == null);
if (nw != null) { if (nw != null) {
@@ -217,28 +246,6 @@ public class FractionEditorView extends BorderPane {
reload(); reload();
} }
private void saveCurrentFraction() {
if (current == null) return;
saveFormToFraction(current);
if (current.getFractionId() == null) {
new Alert(Alert.AlertType.ERROR,
"Fraktion hat keine UUID – bitte neu erstellen.", ButtonType.OK).showAndWait();
return;
}
try {
FractionIO.save(current, fractionDir);
} catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
return;
}
reload();
final UUID fid = current.getFractionId();
fractions.stream()
.filter(f -> fid.equals(f.getFractionId()))
.findFirst()
.ifPresent(listView.getSelectionModel()::select);
}
public void reload() { public void reload() {
fractions.setAll(FractionIO.loadAll(fractionDir)); fractions.setAll(FractionIO.loadAll(fractionDir));
} }

View File

@@ -9,6 +9,7 @@ import de.blight.common.model.ItemIO;
import de.blight.common.model.ItemSubCategory; import de.blight.common.model.ItemSubCategory;
import de.blight.common.model.ObjectReference; import de.blight.common.model.ObjectReference;
import de.blight.common.model.TextReference; import de.blight.common.model.TextReference;
import javafx.animation.PauseTransition;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
@@ -18,6 +19,7 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -51,6 +53,12 @@ public class ItemEditorView extends BorderPane {
private VBox consumablesSection; private VBox consumablesSection;
private VBox effectsRows; private VBox effectsRows;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
{
savePause.setOnFinished(e -> persistCurrent());
}
public ItemEditorView(Path itemDir) { public ItemEditorView(Path itemDir) {
this.itemDir = itemDir; this.itemDir = itemDir;
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot
@@ -188,6 +196,7 @@ public class ItemEditorView extends BorderPane {
if (wasFocused && !isFocused) onIdCommitted(); if (wasFocused && !isFocused) onIdCommitted();
}); });
idField.setOnAction(e -> onIdCommitted()); idField.setOnAction(e -> onIdCommitted());
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
catCombo = new ComboBox<>(); catCombo = new ComboBox<>();
catCombo.getItems().addAll(ItemCategory.values()); catCombo.getItems().addAll(ItemCategory.values());
@@ -237,15 +246,20 @@ public class ItemEditorView extends BorderPane {
} else { } else {
subCatCombo.setValue(null); subCatCombo.setValue(null);
} }
scheduleSave();
}); });
subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave());
nameField = new TextField(); nameField = new TextField();
nameField.setPromptText("TextReference-Schlüssel"); nameField.setPromptText("TextReference-Schlüssel");
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
descField = new TextField(); descField = new TextField();
descField.setPromptText("TextReference-Schlüssel"); descField.setPromptText("TextReference-Schlüssel");
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
goldSpinner = new Spinner<>(0, 999999, 0); goldSpinner = new Spinner<>(0, 999999, 0);
goldSpinner.setEditable(true); goldSpinner.setEditable(true);
goldSpinner.setMaxWidth(Double.MAX_VALUE); goldSpinner.setMaxWidth(Double.MAX_VALUE);
goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
modelRefField = new TextField(); modelRefField = new TextField();
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
@@ -287,13 +301,9 @@ public class ItemEditorView extends BorderPane {
consumableCheck.selectedProperty().addListener((obs, wasSelected, isSelected) -> { consumableCheck.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
consumablesSection.setVisible(isSelected); consumablesSection.setVisible(isSelected);
consumablesSection.setManaged(isSelected); consumablesSection.setManaged(isSelected);
scheduleSave();
}); });
Button saveBtn = new Button("Item speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrentItem());
form.getChildren().addAll( form.getChildren().addAll(
sectionTitle("Item"), sectionTitle("Item"),
new Separator(), new Separator(),
@@ -310,9 +320,7 @@ public class ItemEditorView extends BorderPane {
row("Modell:", modelFullRow), row("Modell:", modelFullRow),
new Separator(), new Separator(),
consumableCheck, consumableCheck,
consumablesSection, consumablesSection
new Separator(),
saveBtn
); );
return form; return form;
} }
@@ -331,10 +339,32 @@ public class ItemEditorView extends BorderPane {
if (descField.getText().isBlank()) descField.setText(id + ".description"); if (descField.getText().isBlank()) descField.setText(id + ".description");
} }
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() {
savePause.playFromStart();
}
private void persistCurrent() {
if (current == null) { return; }
saveFormToItem(current);
if (current.getItemId() == null || current.getItemId().isBlank()) { return; }
try {
ItemIO.save(current, itemDir);
} catch (IOException e) {
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
}
}
// ── Form load / save ────────────────────────────────────────────────────── // ── Form load / save ──────────────────────────────────────────────────────
private void onItemSelected(Item old, Item nw) { private void onItemSelected(Item old, Item nw) {
if (old != null) saveFormToItem(old); if (old != null) {
saveFormToItem(old);
if (old.getItemId() != null && !old.getItemId().isBlank()) {
try { ItemIO.save(old, itemDir); } catch (IOException e) { /* ignore on navigation */ }
}
}
current = nw; current = nw;
deleteBtn.setDisable(nw == null); deleteBtn.setDisable(nw == null);
if (nw != null) { if (nw != null) {
@@ -424,24 +454,6 @@ public class ItemEditorView extends BorderPane {
reload(); reload();
} }
private void saveCurrentItem() {
if (current == null) return;
saveFormToItem(current);
if (current.getItemId() == null || current.getItemId().isBlank()) {
new Alert(Alert.AlertType.ERROR, "Item-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
return;
}
try {
ItemIO.save(current, itemDir);
} catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
return;
}
String savedId = current.getItemId();
reload();
selectItem(savedId);
}
// ── Effect rows ─────────────────────────────────────────────────────────── // ── Effect rows ───────────────────────────────────────────────────────────
private void addEffectRow(CharacterStat stat, int value) { private void addEffectRow(CharacterStat stat, int value) {

View File

@@ -6,6 +6,7 @@ import de.blight.common.model.TextBundle;
import de.blight.common.model.TextBundleIO; import de.blight.common.model.TextBundleIO;
import de.blight.common.model.TextKeyStore; import de.blight.common.model.TextKeyStore;
import de.blight.common.model.TextRegistry; import de.blight.common.model.TextRegistry;
import javafx.animation.PauseTransition;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@@ -16,6 +17,7 @@ import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.util.Duration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -39,6 +41,12 @@ public class LocalizationEditorView extends BorderPane {
private final TableView<String[]> audioTable = new TableView<>(audioData); private final TableView<String[]> audioTable = new TableView<>(audioData);
private AudioBundle currentAudioBundle = null; private AudioBundle currentAudioBundle = null;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
{
savePause.setOnFinished(e -> saveCurrent());
}
public LocalizationEditorView(Path locDir) { public LocalizationEditorView(Path locDir) {
this.locDir = locDir; this.locDir = locDir;
setStyle("-fx-background-color: #1e1e2e;"); setStyle("-fx-background-color: #1e1e2e;");
@@ -58,13 +66,7 @@ public class LocalizationEditorView extends BorderPane {
Button delLangBtn = new Button("- Sprache"); Button delLangBtn = new Button("- Sprache");
delLangBtn.setOnAction(e -> deleteLanguage()); delLangBtn.setOnAction(e -> deleteLanguage());
Button saveBtn = new Button("Speichern"); HBox toolbar = new HBox(8, langLbl, langCombo, addLangBtn, delLangBtn);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrent());
HBox toolbar = new HBox(8, langLbl, langCombo, addLangBtn, delLangBtn,
new Separator(javafx.geometry.Orientation.VERTICAL),
saveBtn);
toolbar.setPadding(new Insets(8, 12, 8, 12)); toolbar.setPadding(new Insets(8, 12, 8, 12));
toolbar.setAlignment(Pos.CENTER_LEFT); toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;"); toolbar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
@@ -106,13 +108,13 @@ public class LocalizationEditorView extends BorderPane {
TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel"); TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel");
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0])); keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));
keyCol.setCellFactory(TextFieldTableCell.forTableColumn()); keyCol.setCellFactory(TextFieldTableCell.forTableColumn());
keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); table.refresh(); }); keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); table.refresh(); scheduleSave(); });
keyCol.setPrefWidth(280); keyCol.setPrefWidth(280);
TableColumn<String[], String> valCol = new TableColumn<>("Text"); TableColumn<String[], String> valCol = new TableColumn<>("Text");
valCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1])); valCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1]));
valCol.setCellFactory(TextFieldTableCell.forTableColumn()); valCol.setCellFactory(TextFieldTableCell.forTableColumn());
valCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); table.refresh(); }); valCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); table.refresh(); scheduleSave(); });
table.getColumns().addAll(List.of(keyCol, valCol)); table.getColumns().addAll(List.of(keyCol, valCol));
VBox.setVgrow(table, Priority.ALWAYS); VBox.setVgrow(table, Priority.ALWAYS);
@@ -150,13 +152,13 @@ public class LocalizationEditorView extends BorderPane {
TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel"); TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel");
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0])); keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));
keyCol.setCellFactory(TextFieldTableCell.forTableColumn()); keyCol.setCellFactory(TextFieldTableCell.forTableColumn());
keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); audioTable.refresh(); }); keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); audioTable.refresh(); scheduleSave(); });
keyCol.setPrefWidth(220); keyCol.setPrefWidth(220);
TableColumn<String[], String> pathCol = new TableColumn<>("Datei"); TableColumn<String[], String> pathCol = new TableColumn<>("Datei");
pathCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1])); pathCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1]));
pathCol.setCellFactory(TextFieldTableCell.forTableColumn()); pathCol.setCellFactory(TextFieldTableCell.forTableColumn());
pathCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); audioTable.refresh(); }); pathCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); audioTable.refresh(); scheduleSave(); });
TableColumn<String[], Void> browseCol = new TableColumn<>(""); TableColumn<String[], Void> browseCol = new TableColumn<>("");
browseCol.setPrefWidth(36); browseCol.setPrefWidth(36);
@@ -179,6 +181,7 @@ public class LocalizationEditorView extends BorderPane {
if (file != null) { if (file != null) {
audioData.get(idx)[1] = file.getAbsolutePath(); audioData.get(idx)[1] = file.getAbsolutePath();
audioTable.refresh(); audioTable.refresh();
scheduleSave();
} }
}); });
} }
@@ -234,6 +237,7 @@ public class LocalizationEditorView extends BorderPane {
private void addLanguage() { private void addLanguage() {
TextInputDialog dlg = new TextInputDialog(); TextInputDialog dlg = new TextInputDialog();
dlg.initOwner(Dialogs.primaryWindow());
dlg.setTitle("Sprache hinzufuegen"); dlg.setTitle("Sprache hinzufuegen");
dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):"); dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):");
dlg.showAndWait().ifPresent(lang -> { dlg.showAndWait().ifPresent(lang -> {
@@ -245,7 +249,7 @@ public class LocalizationEditorView extends BorderPane {
refreshLangList(); refreshLangList();
langCombo.setValue(lang); langCombo.setValue(lang);
} catch (IOException e) { } catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
} }
}); });
} }
@@ -253,7 +257,7 @@ public class LocalizationEditorView extends BorderPane {
private void deleteLanguage() { private void deleteLanguage() {
String lang = langCombo.getValue(); String lang = langCombo.getValue();
if (lang == null) return; if (lang == null) return;
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, Alert confirm = Dialogs.alert(Alert.AlertType.CONFIRMATION,
"Sprache '" + lang + "' wirklich loeschen?", ButtonType.YES, ButtonType.NO); "Sprache '" + lang + "' wirklich loeschen?", ButtonType.YES, ButtonType.NO);
confirm.showAndWait().ifPresent(bt -> { confirm.showAndWait().ifPresent(bt -> {
if (bt == ButtonType.YES) { if (bt == ButtonType.YES) {
@@ -266,7 +270,7 @@ public class LocalizationEditorView extends BorderPane {
audioData.clear(); audioData.clear();
refreshLangList(); refreshLangList();
} catch (IOException e) { } catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
} }
} }
}); });
@@ -282,6 +286,7 @@ public class LocalizationEditorView extends BorderPane {
if (available.isEmpty()) { if (available.isEmpty()) {
TextInputDialog dlg = new TextInputDialog(); TextInputDialog dlg = new TextInputDialog();
dlg.initOwner(Dialogs.primaryWindow());
dlg.setTitle("Schluessel hinzufuegen"); dlg.setTitle("Schluessel hinzufuegen");
dlg.setHeaderText("Alle bekannten Schluessel bereits vorhanden.\nNeuen Schluessel eingeben:"); dlg.setHeaderText("Alle bekannten Schluessel bereits vorhanden.\nNeuen Schluessel eingeben:");
dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(this::insertKey); dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(this::insertKey);
@@ -289,6 +294,7 @@ public class LocalizationEditorView extends BorderPane {
} }
Dialog<String> dlg = new Dialog<>(); Dialog<String> dlg = new Dialog<>();
dlg.initOwner(Dialogs.primaryWindow());
dlg.setTitle("Schluessel waehlen"); dlg.setTitle("Schluessel waehlen");
dlg.setHeaderText("Noch nicht uebersetzte Schluessel:"); dlg.setHeaderText("Noch nicht uebersetzte Schluessel:");
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
@@ -315,36 +321,45 @@ public class LocalizationEditorView extends BorderPane {
tableData.add(new String[]{key, ""}); tableData.add(new String[]{key, ""});
table.scrollTo(tableData.size() - 1); table.scrollTo(tableData.size() - 1);
table.getSelectionModel().select(tableData.size() - 1); table.getSelectionModel().select(tableData.size() - 1);
scheduleSave();
} }
private void deleteKey() { private void deleteKey() {
String[] sel = table.getSelectionModel().getSelectedItem(); String[] sel = table.getSelectionModel().getSelectedItem();
if (sel != null) tableData.remove(sel); if (sel != null) { tableData.remove(sel); scheduleSave(); }
} }
// Audio-Eintrags-Verwaltung // Audio-Eintrags-Verwaltung
private void addAudioKey() { private void addAudioKey() {
TextInputDialog dlg = new TextInputDialog(); TextInputDialog dlg = new TextInputDialog();
dlg.initOwner(Dialogs.primaryWindow());
dlg.setTitle("Audio-Schluessel hinzufuegen"); dlg.setTitle("Audio-Schluessel hinzufuegen");
dlg.setHeaderText("Schluessel eingeben (muss dem AudioReference-Key im Dialog entsprechen):"); dlg.setHeaderText("Schluessel eingeben (muss dem AudioReference-Key im Dialog entsprechen):");
dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(key -> { dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(key -> {
audioData.add(new String[]{key, ""}); audioData.add(new String[]{key, ""});
audioTable.scrollTo(audioData.size() - 1); audioTable.scrollTo(audioData.size() - 1);
audioTable.getSelectionModel().select(audioData.size() - 1); audioTable.getSelectionModel().select(audioData.size() - 1);
scheduleSave();
}); });
} }
private void deleteAudioKey() { private void deleteAudioKey() {
String[] sel = audioTable.getSelectionModel().getSelectedItem(); String[] sel = audioTable.getSelectionModel().getSelectedItem();
if (sel != null) audioData.remove(sel); if (sel != null) { audioData.remove(sel); scheduleSave(); }
}
// Auto-save
private void scheduleSave() {
savePause.playFromStart();
} }
// Speichern // Speichern
private void saveCurrent() { private void saveCurrent() {
if (currentBundle == null) { if (currentBundle == null) {
new Alert(Alert.AlertType.WARNING, "Keine Sprache ausgewaehlt.", ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.WARNING, "Keine Sprache ausgewaehlt.", ButtonType.OK).showAndWait();
return; return;
} }
Map<String, String> textEntries = new LinkedHashMap<>(); Map<String, String> textEntries = new LinkedHashMap<>();

View File

@@ -3,6 +3,7 @@ package de.blight.editor.ui;
import de.blight.common.LocationIO; import de.blight.common.LocationIO;
import de.blight.common.model.Location; import de.blight.common.model.Location;
import de.blight.common.model.TextReference; import de.blight.common.model.TextReference;
import javafx.animation.PauseTransition;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@@ -10,32 +11,29 @@ import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
/**
* Locations-Verwaltung: Liste links, Formular rechts.
* Alle Locations werden gemeinsam in einer Datei gespeichert (LocationIO).
*/
public class LocationEditorView extends BorderPane { public class LocationEditorView extends BorderPane {
private final ObservableList<Location> locations = FXCollections.observableArrayList(); private static final String PREFIX = "location.";
// ── List ────────────────────────────────────────────────────────────────── private final ObservableList<Location> locations = FXCollections.observableArrayList();
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
private ListView<Location> listView; private ListView<Location> listView;
private Button deleteBtn; private Button deleteBtn;
private Location current = null; private Location current = null;
private boolean reloading = false;
// ── Form fields ─────────────────────────────────────────────────────────── private TextField idField;
private Label nameKeyLabel;
private TextField nameIdField;
private TextField centerXField;
private TextField centerZField;
private TextField radiusField;
private TriggerListEditor triggerEditor; private TriggerListEditor triggerEditor;
private VBox formContainer; private VBox formContainer;
{ savePause.setOnFinished(e -> persistCurrent()); }
public LocationEditorView() { public LocationEditorView() {
setStyle("-fx-background-color: #1e1e2e;"); setStyle("-fx-background-color: #1e1e2e;");
@@ -46,6 +44,18 @@ public class LocationEditorView extends BorderPane {
setCenter(split); setCenter(split);
} }
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() { savePause.playFromStart(); }
private void persistCurrent() {
if (current == null || reloading) return;
saveFormToLocation(current);
if (current.getId() == null || current.getId().isBlank()) return;
persist();
listView.refresh();
}
// ── List panel ──────────────────────────────────────────────────────────── // ── List panel ────────────────────────────────────────────────────────────
private VBox buildListPanel() { private VBox buildListPanel() {
@@ -55,7 +65,9 @@ public class LocationEditorView extends BorderPane {
@Override protected void updateItem(Location loc, boolean empty) { @Override protected void updateItem(Location loc, boolean empty) {
super.updateItem(loc, empty); super.updateItem(loc, empty);
if (empty || loc == null) { setText(null); setStyle(""); return; } if (empty || loc == null) { setText(null); setStyle(""); return; }
setText(loc.getId().isBlank() ? "—" : loc.getId()); String shortId = loc.getId().startsWith(PREFIX)
? loc.getId().substring(PREFIX.length()) : loc.getId();
setText(shortId.isBlank() ? "—" : shortId);
setStyle("-fx-text-fill: #dddddd;" setStyle("-fx-text-fill: #dddddd;"
+ " -fx-border-color: transparent transparent transparent #66aacc;" + " -fx-border-color: transparent transparent transparent #66aacc;"
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;"); + " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
@@ -108,30 +120,28 @@ public class LocationEditorView extends BorderPane {
form.setPadding(new Insets(12)); form.setPadding(new Insets(12));
form.setStyle("-fx-background-color: #252535;"); form.setStyle("-fx-background-color: #252535;");
nameIdField = field("z. B. location.village"); idField = field("z. B. village");
centerXField = field("X-Koordinate"); nameKeyLabel = new Label("Name-Key: —");
centerZField = field("Z-Koordinate"); nameKeyLabel.setStyle("-fx-text-fill: #aaaaaa; -fx-font-style: italic; -fx-font-size: 11;");
radiusField = field("Radius in Meter"); idField.textProperty().addListener((obs, o, n) -> {
String key = n == null || n.isBlank() ? "" : PREFIX + n.trim() + ".name";
nameKeyLabel.setText("Name-Key: " + (key.isBlank() ? "—" : key));
scheduleSave();
});
idField.focusedProperty().addListener((obs, was, is) -> {
if (!is) { savePause.stop(); persistCurrent(); }
});
triggerEditor = new TriggerListEditor(List.of(), () -> {}); triggerEditor = new TriggerListEditor(List.of(), () -> {});
Button saveBtn = new Button("Location speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrent());
form.getChildren().addAll( form.getChildren().addAll(
sectionTitle("Kennung & Position"), sectionTitle("Kennung"),
new Separator(), new Separator(),
row("Name-ID:", nameIdField), row("ID:", idField),
row("Mitte X:", centerXField), nameKeyLabel,
row("Mitte Z:", centerZField),
row("Radius:", radiusField),
sectionTitle("Trigger"), sectionTitle("Trigger"),
new Separator(), new Separator(),
triggerEditor, triggerEditor
new Separator(),
saveBtn
); );
return form; return form;
} }
@@ -139,7 +149,8 @@ public class LocationEditorView extends BorderPane {
// ── Form load / save ────────────────────────────────────────────────────── // ── Form load / save ──────────────────────────────────────────────────────
private void onSelected(Location old, Location nw) { private void onSelected(Location old, Location nw) {
if (old != null) saveFormToLocation(old); if (reloading) return;
if (old != null) { saveFormToLocation(old); persist(); }
current = nw; current = nw;
deleteBtn.setDisable(nw == null); deleteBtn.setDisable(nw == null);
if (nw != null) { formContainer.setDisable(false); loadForm(nw); } if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
@@ -147,38 +158,34 @@ public class LocationEditorView extends BorderPane {
} }
private void loadForm(Location loc) { private void loadForm(Location loc) {
nameIdField.setText(loc.getId()); String fullId = loc.getId();
centerXField.setText(String.valueOf(loc.getCenterX())); String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
centerZField.setText(String.valueOf(loc.getCenterZ())); idField.setText(shortId);
radiusField.setText(String.valueOf(loc.getRadius())); nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
int idx = formContainer.getChildren().indexOf(triggerEditor); int idx = formContainer.getChildren().indexOf(triggerEditor);
triggerEditor = new TriggerListEditor( triggerEditor = new TriggerListEditor(
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> {}); loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> scheduleSave());
if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor); if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor);
} }
private void saveFormToLocation(Location loc) { private void saveFormToLocation(Location loc) {
String nameId = nameIdField.getText().trim(); String shortId = idField.getText().trim();
loc.setName(nameId.isBlank() ? null : new TextReference(nameId)); String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
loc.setCenterX(parseFloat(centerXField.getText())); loc.setName(fullId.isBlank() ? null : new TextReference(fullId));
loc.setCenterZ(parseFloat(centerZField.getText()));
loc.setRadius(parseFloat(radiusField.getText()));
loc.setTriggers(triggerEditor.getTriggers()); loc.setTriggers(triggerEditor.getTriggers());
} }
private void clearForm() { private void clearForm() {
nameIdField.clear(); idField.clear();
centerXField.clear(); nameKeyLabel.setText("Name-Key: —");
centerZField.clear();
radiusField.clear();
} }
// ── List operations ─────────────────────────────────────────────────────── // ── List operations ───────────────────────────────────────────────────────
private void createLocation() { private void createLocation() {
Location loc = new Location(); Location loc = new Location();
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis())); loc.setName(new TextReference(PREFIX + "neu_" + System.currentTimeMillis()));
locations.add(loc); locations.add(loc);
listView.getSelectionModel().select(loc); listView.getSelectionModel().select(loc);
} }
@@ -194,26 +201,10 @@ public class LocationEditorView extends BorderPane {
reload(); reload();
} }
private void saveCurrent() {
if (current == null) return;
saveFormToLocation(current);
if (current.getId().isBlank()) {
new Alert(Alert.AlertType.ERROR, "Name-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
return;
}
String savedId = current.getId();
persist();
reload();
locations.stream()
.filter(l -> savedId.equals(l.getId()))
.findFirst()
.ifPresent(listView.getSelectionModel()::select);
}
private void persist() { private void persist() {
try { LocationIO.save(locations); } try { LocationIO.save(locations); }
catch (IOException e) { catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
} }
} }
@@ -224,11 +215,6 @@ public class LocationEditorView extends BorderPane {
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
private static float parseFloat(String s) {
try { return Float.parseFloat(s.trim().replace(',', '.')); }
catch (NumberFormatException ignored) { return 0f; }
}
private static Label sectionTitle(String text) { private static Label sectionTitle(String text) {
Label l = new Label(text); Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;"); l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
@@ -237,7 +223,7 @@ public class LocationEditorView extends BorderPane {
private static HBox row(String labelText, Node control) { private static HBox row(String labelText, Node control) {
Label lbl = new Label(labelText); Label lbl = new Label(labelText);
lbl.setMinWidth(80); lbl.setMinWidth(50);
lbl.setStyle("-fx-text-fill: #aaa;"); lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(control, Priority.ALWAYS); HBox.setHgrow(control, Priority.ALWAYS);
HBox box = new HBox(8, lbl, control); HBox box = new HBox(8, lbl, control);

View File

@@ -115,11 +115,9 @@ public class MapObjectsView extends VBox {
// ── Löschen ─────────────────────────────────────────────────────────────── // ── Löschen ───────────────────────────────────────────────────────────────
private void confirmAndDelete(String label, Entry entry) { private void confirmAndDelete(String label, Entry entry) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION); Alert alert = Dialogs.alert(Alert.AlertType.CONFIRMATION, label, ButtonType.YES, ButtonType.NO);
alert.setTitle("Objekt löschen"); alert.setTitle("Objekt löschen");
alert.setHeaderText("Objekt wirklich löschen?"); alert.setHeaderText("Objekt wirklich löschen?");
alert.setContentText(label);
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
Optional<ButtonType> result = alert.showAndWait(); Optional<ButtonType> result = alert.showAndWait();
if (result.isEmpty() || result.get() != ButtonType.YES) return; if (result.isEmpty() || result.get() != ButtonType.YES) return;
@@ -128,8 +126,7 @@ public class MapObjectsView extends VBox {
onDelete.execute(entry.placedObj(), entry.index(), entry.toolHint()); onDelete.execute(entry.placedObj(), entry.index(), entry.toolHint());
refresh(); refresh();
} catch (Exception ex) { } catch (Exception ex) {
Alert err = new Alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK).showAndWait();
err.showAndWait();
} }
} }
@@ -243,12 +240,12 @@ public class MapObjectsView extends VBox {
private void loadAreas() { private void loadAreas() {
try { try {
List<PlacedArea> list = AreaIO.load(); List<PlacedArea> list = AreaIO.load();
TreeItem<String> group = group("Bereiche", list.size()); TreeItem<String> group = group("Areas", list.size());
for (int idx = 0; idx < list.size(); idx++) { for (int idx = 0; idx < list.size(); idx++) {
PlacedArea a = list.get(idx); PlacedArea a = list.get(idx);
float cx = centroid(a.pointsX()), cz = centroid(a.pointsZ()); float cx = centroid(a.pointsX()), cz = centroid(a.pointsZ());
String label = a.nameId() != null && !a.nameId().isBlank() String label = a.areaId() != null && !a.areaId().isBlank()
? a.nameId() : "Bereich #" + (idx + 1); ? a.areaId() : "Area #" + (idx + 1);
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz)); TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
entryMap.put(item, new Entry(cx, Float.NaN, cz, "area", a, idx)); entryMap.put(item, new Entry(cx, Float.NaN, cz, "area", a, idx));
group.getChildren().add(item); group.getChildren().add(item);

View File

@@ -34,6 +34,7 @@ public class MaterialChooser extends Dialog<String> {
public MaterialChooser(Path matDefsRoot) { public MaterialChooser(Path matDefsRoot) {
setTitle("Material auswählen"); setTitle("Material auswählen");
initModality(Modality.APPLICATION_MODAL); initModality(Modality.APPLICATION_MODAL);
initOwner(Dialogs.primaryWindow());
setResizable(true); setResizable(true);
VBox contentBox = new VBox(12); VBox contentBox = new VBox(12);

View File

@@ -39,6 +39,7 @@ public class ModelChooser extends Dialog<String> {
this.assetRoot = assetRoot; this.assetRoot = assetRoot;
setTitle("Modell auswählen"); setTitle("Modell auswählen");
initModality(Modality.APPLICATION_MODAL); initModality(Modality.APPLICATION_MODAL);
initOwner(Dialogs.primaryWindow());
setResizable(true); setResizable(true);
contentBox.setPadding(new Insets(4)); contentBox.setPadding(new Insets(4));

View File

@@ -237,7 +237,7 @@ public class MonologueEditorView extends SplitPane {
try { try {
MonologueIO.save(new ArrayList<>(monologues)); MonologueIO.save(new ArrayList<>(monologues));
} catch (IOException e) { } catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
} }
} }
@@ -253,7 +253,7 @@ public class MonologueEditorView extends SplitPane {
private void deleteMonologue() { private void deleteMonologue() {
Monologue sel = listView.getSelectionModel().getSelectedItem(); Monologue sel = listView.getSelectionModel().getSelectedItem();
if (sel == null) return; if (sel == null) return;
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, Alert confirm = Dialogs.alert(Alert.AlertType.CONFIRMATION,
"Monolog '" + sel.getId() + "' wirklich löschen?", ButtonType.YES, ButtonType.NO); "Monolog '" + sel.getId() + "' wirklich löschen?", ButtonType.YES, ButtonType.NO);
confirm.showAndWait().ifPresent(bt -> { confirm.showAndWait().ifPresent(bt -> {
if (bt == ButtonType.YES) { if (bt == ButtonType.YES) {
@@ -296,6 +296,7 @@ public class MonologueEditorView extends SplitPane {
Dialog<String> dlg = new Dialog<>(); Dialog<String> dlg = new Dialog<>();
dlg.setTitle("Quest auswählen"); dlg.setTitle("Quest auswählen");
dlg.initModality(Modality.APPLICATION_MODAL); dlg.initModality(Modality.APPLICATION_MODAL);
dlg.initOwner(Dialogs.primaryWindow());
ListView<Quest> chooser = new ListView<>(); ListView<Quest> chooser = new ListView<>();
chooser.setCellFactory(lv -> new ListCell<>() { chooser.setCellFactory(lv -> new ListCell<>() {

View File

@@ -7,6 +7,7 @@ import de.blight.common.model.NPC;
import de.blight.common.model.Location; import de.blight.common.model.Location;
import de.blight.common.model.TextReference; import de.blight.common.model.TextReference;
import de.blight.common.model.quests.*; import de.blight.common.model.quests.*;
import javafx.animation.PauseTransition;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@@ -14,6 +15,7 @@ import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -43,13 +45,19 @@ public class QuestEditorView extends BorderPane {
private ComboBox<String> typeCombo; private ComboBox<String> typeCombo;
private VBox dynamicArea; private VBox dynamicArea;
private VBox formContainer; private VBox formContainer;
private Button saveBtn;
// ── Type-specific fields ────────────────────────────────────────────────── // ── Type-specific fields ──────────────────────────────────────────────────
private TextField f1, f2, f3; private TextField f1, f2, f3;
private Spinner<Integer> countSpinner; private Spinner<Integer> countSpinner;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
private boolean reloading = false;
{
savePause.setOnFinished(e -> persistCurrent());
}
public QuestEditorView(Path questDir) { public QuestEditorView(Path questDir) {
this.questDir = questDir; this.questDir = questDir;
setStyle("-fx-background-color: #1e1e2e;"); setStyle("-fx-background-color: #1e1e2e;");
@@ -134,17 +142,22 @@ public class QuestEditorView extends BorderPane {
idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> { idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
if (!isFocused) autoFillTextRefs(); if (!isFocused) autoFillTextRefs();
}); });
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
xpSpinner = new Spinner<>(0, 99999, 0); xpSpinner = new Spinner<>(0, 99999, 0);
xpSpinner.setEditable(true); xpSpinner.setEditable(true);
xpSpinner.setMaxWidth(Double.MAX_VALUE); xpSpinner.setMaxWidth(Double.MAX_VALUE);
xpSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
textField = new TextField(); textField = new TextField();
textField.setPromptText("TextReference-Schlüssel"); textField.setPromptText("TextReference-Schlüssel");
textField.textProperty().addListener((obs, o, n) -> scheduleSave());
descField = new TextField(); descField = new TextField();
descField.setPromptText("TextReference-Schlüssel"); descField.setPromptText("TextReference-Schlüssel");
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
successField = new TextField(); successField = new TextField();
successField.setPromptText("TextReference-Schlüssel"); successField.setPromptText("TextReference-Schlüssel");
successField.textProperty().addListener((obs, o, n) -> scheduleSave());
form.getChildren().addAll( form.getChildren().addAll(
sectionTitle("Quest"), sectionTitle("Quest"),
@@ -163,7 +176,7 @@ public class QuestEditorView extends BorderPane {
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest"); typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
typeCombo.setPromptText("Typ auswählen…"); typeCombo.setPromptText("Typ auswählen…");
typeCombo.setMaxWidth(Double.MAX_VALUE); typeCombo.setMaxWidth(Double.MAX_VALUE);
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue())); typeCombo.setOnAction(e -> { rebuildDynamicArea(typeCombo.getValue()); scheduleSave(); });
dynamicArea = new VBox(6); dynamicArea = new VBox(6);
@@ -174,12 +187,6 @@ public class QuestEditorView extends BorderPane {
new Separator() new Separator()
); );
saveBtn = new Button("Quest speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrentQuest());
form.getChildren().add(saveBtn);
return form; return form;
} }
@@ -243,10 +250,40 @@ public class QuestEditorView extends BorderPane {
} }
} }
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() {
savePause.playFromStart();
}
private void persistCurrent() {
if (current == null || reloading) { return; }
Quest built = buildQuestFromForm();
if (built == null || built.getQuestId() == null || built.getQuestId().isBlank()) { return; }
int idx = quests.indexOf(current);
if (idx >= 0) { quests.set(idx, built); }
else { quests.add(built); }
current = built;
try {
QuestIO.save(built, questDir);
} catch (IOException e) {
showError("Fehler beim Speichern: " + e.getMessage());
}
}
// ── Form load / save ────────────────────────────────────────────────────── // ── Form load / save ──────────────────────────────────────────────────────
private void onQuestSelected(Quest old, Quest nw) { private void onQuestSelected(Quest old, Quest nw) {
if (old != null) saveFormToQuest(old); if (reloading) { return; }
if (old != null) {
saveFormToQuest(old);
Quest built = buildQuestFromForm();
if (built != null && built.getQuestId() != null && !built.getQuestId().isBlank()) {
int idx = quests.indexOf(old);
if (idx >= 0) { quests.set(idx, built); }
try { QuestIO.save(built, questDir); } catch (IOException e) { /* ignore on navigation */ }
}
}
current = nw; current = nw;
deleteBtn.setDisable(nw == null); deleteBtn.setDisable(nw == null);
if (nw != null) { if (nw != null) {
@@ -406,30 +443,6 @@ public class QuestEditorView extends BorderPane {
reload(); reload();
} }
private void saveCurrentQuest() {
Quest built = buildQuestFromForm();
if (built == null) {
showError("Bitte einen Typ wählen.");
return;
}
if (built.getQuestId() == null || built.getQuestId().isBlank()) {
showError("Quest-ID darf nicht leer sein.");
return;
}
int idx = quests.indexOf(current);
if (idx >= 0) quests.set(idx, built);
else quests.add(built);
current = built;
listView.getSelectionModel().select(built);
try {
QuestIO.save(built, questDir);
} catch (IOException e) {
showError("Fehler beim Speichern: " + e.getMessage());
return;
}
reload();
}
public void reload() { public void reload() {
List<Quest> loaded = QuestIO.loadAll(questDir); List<Quest> loaded = QuestIO.loadAll(questDir);
quests.setAll(loaded); quests.setAll(loaded);
@@ -467,7 +480,6 @@ public class QuestEditorView extends BorderPane {
} }
private void showError(String msg) { private void showError(String msg) {
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK); Dialogs.alert(Alert.AlertType.ERROR, msg, ButtonType.OK).showAndWait();
a.showAndWait();
} }
} }

View File

@@ -1,6 +1,7 @@
package de.blight.editor.ui; package de.blight.editor.ui;
import de.blight.common.model.*; import de.blight.common.model.*;
import javafx.animation.PauseTransition;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
@@ -10,6 +11,7 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -49,6 +51,12 @@ public class RecipeEditorView extends BorderPane {
private Spinner<Integer> engineeringSpinner; private Spinner<Integer> engineeringSpinner;
private VBox formContainer; private VBox formContainer;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
{
savePause.setOnFinished(e -> persistCurrent());
}
public RecipeEditorView(Path recipeDir) { public RecipeEditorView(Path recipeDir) {
this.recipeDir = recipeDir; this.recipeDir = recipeDir;
this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER); this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
@@ -129,6 +137,7 @@ public class RecipeEditorView extends BorderPane {
createsField = new TextField(); createsField = new TextField();
createsField.setPromptText("Item-ID des erstellten Items"); createsField.setPromptText("Item-ID des erstellten Items");
createsField.textProperty().addListener((obs, o, n) -> scheduleSave());
componentsList = new ListView<>(); componentsList = new ListView<>();
componentsList.setPrefHeight(110); componentsList.setPrefHeight(110);
@@ -156,7 +165,7 @@ public class RecipeEditorView extends BorderPane {
} }
tableCombo.setValue(NO_TABLE); tableCombo.setValue(NO_TABLE);
tableCombo.setMaxWidth(Double.MAX_VALUE); tableCombo.setMaxWidth(Double.MAX_VALUE);
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue())); tableCombo.setOnAction(e -> { updateRequirementRows(tableCombo.getValue()); scheduleSave(); });
alchemySpinner = lvlSpinner(); alchemySpinner = lvlSpinner();
enchantingSpinner = lvlSpinner(); enchantingSpinner = lvlSpinner();
@@ -168,11 +177,6 @@ public class RecipeEditorView extends BorderPane {
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner); smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner); engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner);
Button saveBtn = new Button("Rezept speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
saveBtn.setOnAction(e -> saveCurrentRecipe());
form.getChildren().addAll( form.getChildren().addAll(
sectionTitle("Ergebnis"), sectionTitle("Ergebnis"),
new Separator(), new Separator(),
@@ -186,9 +190,7 @@ public class RecipeEditorView extends BorderPane {
alchemyRow, alchemyRow,
enchantingRow, enchantingRow,
smitheryRow, smitheryRow,
engineeringRow, engineeringRow
new Separator(),
saveBtn
); );
updateRequirementRows(NO_TABLE); updateRequirementRows(NO_TABLE);
@@ -216,10 +218,38 @@ public class RecipeEditorView extends BorderPane {
row.setManaged(visible); row.setManaged(visible);
} }
// ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() {
savePause.playFromStart();
}
private void persistCurrent() {
if (current == null) { return; }
saveFormToRecipe(current);
String newFileId = RecipeIO.fileId(current);
if (newFileId.startsWith("unbenanntes")) { return; }
try {
if (oldFileId != null && !oldFileId.equals(newFileId)) {
RecipeIO.delete(oldFileId, recipeDir);
}
RecipeIO.save(current, recipeDir);
oldFileId = newFileId;
} catch (IOException e) {
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
}
}
// ── Form load / save ────────────────────────────────────────────────────── // ── Form load / save ──────────────────────────────────────────────────────
private void onRecipeSelected(Recipe old, Recipe nw) { private void onRecipeSelected(Recipe old, Recipe nw) {
if (old != null) saveFormToRecipe(old); if (old != null) {
saveFormToRecipe(old);
String fid = RecipeIO.fileId(old);
if (!fid.startsWith("unbenanntes")) {
try { RecipeIO.save(old, recipeDir); } catch (IOException e) { /* ignore on navigation */ }
}
}
current = nw; current = nw;
oldFileId = nw != null ? RecipeIO.fileId(nw) : null; oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
deleteBtn.setDisable(nw == null); deleteBtn.setDisable(nw == null);
@@ -342,38 +372,11 @@ public class RecipeEditorView extends BorderPane {
reload(); reload();
} }
private void saveCurrentRecipe() {
if (current == null) return;
saveFormToRecipe(current);
String newFileId = RecipeIO.fileId(current);
if (newFileId.startsWith("unbenanntes")) {
new Alert(Alert.AlertType.ERROR,
"Item-ID des erstellten Items darf nicht leer sein.", ButtonType.OK).showAndWait();
return;
}
try {
if (oldFileId != null && !oldFileId.equals(newFileId)) {
RecipeIO.delete(oldFileId, recipeDir);
}
RecipeIO.save(current, recipeDir);
oldFileId = newFileId;
} catch (IOException e) {
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
return;
}
reload();
final String fid = newFileId;
recipes.stream()
.filter(r -> fid.equals(RecipeIO.fileId(r)))
.findFirst()
.ifPresent(listView.getSelectionModel()::select);
}
private void addComponent() { private void addComponent() {
Dialog<String> dlg = new Dialog<>(); Dialog<String> dlg = new Dialog<>();
dlg.setTitle("Zutat hinzufügen"); dlg.setTitle("Zutat hinzufügen");
dlg.initModality(Modality.APPLICATION_MODAL); dlg.initModality(Modality.APPLICATION_MODAL);
dlg.initOwner(Dialogs.primaryWindow());
TextField itemIdField = new TextField(); TextField itemIdField = new TextField();
itemIdField.setPromptText("Item-ID"); itemIdField.setPromptText("Item-ID");

View File

@@ -462,6 +462,7 @@ public class RoutineEditorView extends VBox {
private void addRoutine() { private void addRoutine() {
TextInputDialog dlg = new TextInputDialog("Routine " + (routines.size() + 1)); TextInputDialog dlg = new TextInputDialog("Routine " + (routines.size() + 1));
dlg.initOwner(Dialogs.primaryWindow());
dlg.setHeaderText("Name des Tagesablaufs:"); dlg.setHeaderText("Name des Tagesablaufs:");
dlg.showAndWait().ifPresent(name -> { dlg.showAndWait().ifPresent(name -> {
if (name.isBlank()) return; if (name.isBlank()) return;
@@ -475,6 +476,7 @@ public class RoutineEditorView extends VBox {
private void renameRoutine() { private void renameRoutine() {
if (activeRoutine == null) return; if (activeRoutine == null) return;
TextInputDialog dlg = new TextInputDialog(activeRoutine.getName()); TextInputDialog dlg = new TextInputDialog(activeRoutine.getName());
dlg.initOwner(Dialogs.primaryWindow());
dlg.setHeaderText("Neuer Name:"); dlg.setHeaderText("Neuer Name:");
dlg.showAndWait().ifPresent(name -> { dlg.showAndWait().ifPresent(name -> {
if (!name.isBlank()) { if (!name.isBlank()) {

View File

@@ -0,0 +1,166 @@
package de.blight.editor.ui;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.stage.Modality;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
/**
* List-based sound/music picker dialog.
*
* Two modes:
* MUSIC — scans only {@code audio/music/} inside the asset root.
* ALL — scans the entire {@code audio/} tree.
*
* Returns the selected JME asset path (relative to asset root) or {@code null} on cancel.
* Double-click or OK confirms the selection; a small play button previews the file.
*/
public class SoundChooser extends Dialog<String> {
public enum Mode { MUSIC, ALL }
private static final Set<String> AUDIO_EXTS = Set.of(".ogg", ".wav", ".mp3");
private final List<String> allPaths = new ArrayList<>();
private final ListView<String> listView = new ListView<>();
private final TextField filterField = new TextField();
private MediaPlayer preview = null;
private String selected = null;
public SoundChooser(Path assetRoot, Mode mode) {
setTitle(mode == Mode.MUSIC ? "Musik auswählen" : "Sound auswählen");
initModality(Modality.APPLICATION_MODAL);
initOwner(Dialogs.primaryWindow());
setResizable(true);
// ── scan files ────────────────────────────────────────────────────────
if (assetRoot != null) {
Path scanRoot = mode == Mode.MUSIC
? assetRoot.resolve("audio/music")
: assetRoot.resolve("audio");
if (Files.isDirectory(scanRoot)) {
try (Stream<Path> walk = Files.walk(scanRoot)) {
walk.filter(Files::isRegularFile)
.filter(p -> isAudioFile(p.getFileName().toString()))
.sorted(Comparator.comparing(p -> assetRoot.relativize(p).toString().toLowerCase()))
.forEach(p -> allPaths.add(assetRoot.relativize(p).toString().replace('\\', '/')));
} catch (Exception ignored) {}
}
}
listView.getItems().setAll(allPaths);
listView.setPrefHeight(420);
// ── filter ────────────────────────────────────────────────────────────
filterField.setPromptText("Filtern…");
filterField.textProperty().addListener((obs, o, n) -> applyFilter(n == null ? "" : n));
// ── preview bar ───────────────────────────────────────────────────────
Button playBtn = new Button("▶");
Button stopBtn = new Button("■");
stopBtn.setDisable(true);
Label previewLbl = new Label("(nichts gewählt)");
previewLbl.setStyle("-fx-text-fill: #666; -fx-font-size: 11;");
playBtn.setOnAction(e -> {
stopPreview();
String sel = listView.getSelectionModel().getSelectedItem();
if (sel == null || assetRoot == null) return;
Path file = assetRoot.resolve(sel.replace('/', java.io.File.separatorChar));
try {
Media media = new Media(file.toUri().toString());
preview = new MediaPlayer(media);
preview.setOnEndOfMedia(() -> {
stopBtn.setDisable(true);
playBtn.setDisable(false);
});
preview.play();
stopBtn.setDisable(false);
playBtn.setDisable(true);
} catch (Exception ex) {
previewLbl.setText("Fehler: " + ex.getMessage());
}
});
stopBtn.setOnAction(e -> {
stopPreview();
stopBtn.setDisable(true);
playBtn.setDisable(false);
});
HBox previewBar = new HBox(6, playBtn, stopBtn, previewLbl);
previewBar.setAlignment(Pos.CENTER_LEFT);
previewBar.setPadding(new Insets(4, 0, 0, 0));
// ── empty hint ────────────────────────────────────────────────────────
if (allPaths.isEmpty()) {
String hint = mode == Mode.MUSIC
? "Keine Musik gefunden.\nDateien nach audio/music/ importieren."
: "Keine Audio-Dateien gefunden.";
listView.setPlaceholder(new Label(hint));
}
// ── layout ────────────────────────────────────────────────────────────
VBox root = new VBox(8, filterField, listView, previewBar);
root.setPadding(new Insets(10));
getDialogPane().setContent(root);
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
getDialogPane().setPrefSize(480, 540);
// ── selection tracking (buttons must exist before lookup) ─────────────
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
if (okBtn != null) okBtn.setDisable(true);
listView.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> {
selected = n;
previewLbl.setText(n != null ? lastName(n) : "(nichts gewählt)");
stopPreview(); stopBtn.setDisable(true); playBtn.setDisable(n == null);
if (okBtn != null) okBtn.setDisable(n == null);
});
listView.setOnMouseClicked(e -> {
if (e.getClickCount() == 2 && selected != null) {
if (okBtn != null) okBtn.fire();
}
});
setOnHidden(e -> stopPreview());
setResultConverter(btn -> btn == ButtonType.OK ? selected : null);
}
// ── helpers ───────────────────────────────────────────────────────────────
private void applyFilter(String raw) {
String lo = raw.toLowerCase();
if (lo.isBlank()) {
listView.getItems().setAll(allPaths);
} else {
listView.getItems().setAll(
allPaths.stream().filter(p -> p.toLowerCase().contains(lo)).toList());
}
}
private void stopPreview() {
if (preview != null) {
preview.stop();
preview.dispose();
preview = null;
}
}
private static boolean isAudioFile(String name) {
String lo = name.toLowerCase();
return AUDIO_EXTS.stream().anyMatch(lo::endsWith);
}
private static String lastName(String path) {
int i = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
return i >= 0 ? path.substring(i + 1) : path;
}
}

View File

@@ -73,6 +73,7 @@ public class TextureChooser extends Dialog<String> {
public TextureChooser(Path assetRoot, boolean includeJmeBuiltin) { public TextureChooser(Path assetRoot, boolean includeJmeBuiltin) {
setTitle("Textur auswählen"); setTitle("Textur auswählen");
initModality(Modality.APPLICATION_MODAL); initModality(Modality.APPLICATION_MODAL);
initOwner(Dialogs.primaryWindow());
setResizable(true); setResizable(true);
contentBox.setPadding(new Insets(4)); contentBox.setPadding(new Insets(4));

View File

@@ -5,6 +5,8 @@ import de.blight.common.model.Monologue;
import de.blight.common.model.QuestRef; import de.blight.common.model.QuestRef;
import de.blight.common.model.Status; import de.blight.common.model.Status;
import de.blight.common.model.trigger.*; import de.blight.common.model.trigger.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
@@ -12,19 +14,9 @@ import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.Modality; import javafx.stage.Modality;
import java.util.ArrayList;
import java.util.UUID; import java.util.UUID;
/**
* Modal-Dialog zum Anlegen oder Bearbeiten eines {@link Trigger}.
*
* <p>Ablauf:
* <ol>
* <li>Typ-ComboBox wählen</li>
* <li>Typ-spezifische Felder füllen</li>
* <li>Kapitel-Anforderung (optional)</li>
* </ol>
* Rückgabewert: der fertig gebaute {@link Trigger} oder {@code null} bei Abbruch.
*/
public class TriggerDialog extends Dialog<Trigger> { public class TriggerDialog extends Dialog<Trigger> {
private static final String TYPE_QUEST = "Quest starten"; private static final String TYPE_QUEST = "Quest starten";
@@ -32,6 +24,7 @@ public class TriggerDialog extends Dialog<Trigger> {
private static final String TYPE_FRACTION = "Fraktions-Status ändern"; private static final String TYPE_FRACTION = "Fraktions-Status ändern";
private static final String TYPE_ROUTINE = "Routine ändern"; private static final String TYPE_ROUTINE = "Routine ändern";
private static final String TYPE_MONOLOGUE = "Monolog starten"; private static final String TYPE_MONOLOGUE = "Monolog starten";
private static final String TYPE_NPC_DIALOG = "NPC-Dialog starten";
// Gemeinsam // Gemeinsam
private final ComboBox<String> typeCombo = new ComboBox<>(); private final ComboBox<String> typeCombo = new ComboBox<>();
@@ -56,33 +49,89 @@ public class TriggerDialog extends Dialog<Trigger> {
// Monolog // Monolog
private ComboBox<String> monologueIdCombo; private ComboBox<String> monologueIdCombo;
/** Öffnet den Dialog für einen neuen Trigger. */ // NPC-Dialog
private TextField npcDialogIdField;
// Bedingungen
private final ComboBox<String> conditionModeCombo = new ComboBox<>();
private final ObservableList<Condition> conditions = FXCollections.observableArrayList();
private final ListView<Condition> conditionList = new ListView<>(conditions);
public TriggerDialog() { public TriggerDialog() {
this(null); this(null);
} }
/** Öffnet den Dialog im Bearbeitungsmodus mit einem vorhandenen Trigger. */
public TriggerDialog(Trigger existing) { public TriggerDialog(Trigger existing) {
setTitle(existing == null ? "Trigger hinzufügen" : "Trigger bearbeiten"); setTitle(existing == null ? "Trigger hinzufügen" : "Trigger bearbeiten");
initModality(Modality.APPLICATION_MODAL); initModality(Modality.APPLICATION_MODAL);
initOwner(Dialogs.primaryWindow());
setResizable(true); setResizable(true);
typeCombo.getItems().addAll(TYPE_QUEST, TYPE_NPC, TYPE_FRACTION, TYPE_ROUTINE, TYPE_MONOLOGUE); typeCombo.getItems().addAll(TYPE_QUEST, TYPE_NPC, TYPE_FRACTION, TYPE_ROUTINE,
TYPE_MONOLOGUE, TYPE_NPC_DIALOG);
typeCombo.setMaxWidth(Double.MAX_VALUE); typeCombo.setMaxWidth(Double.MAX_VALUE);
typeCombo.setOnAction(e -> rebuildDynamic(typeCombo.getValue())); typeCombo.setOnAction(e -> rebuildDynamic(typeCombo.getValue()));
chapterSpinner.setEditable(true); chapterSpinner.setEditable(true);
chapterSpinner.setPrefWidth(80); chapterSpinner.setPrefWidth(80);
conditionModeCombo.getItems().addAll("ALL – alle müssen gelten", "ANY – mindestens eine");
conditionModeCombo.setValue("ALL – alle müssen gelten");
conditionModeCombo.setMaxWidth(Double.MAX_VALUE);
conditionList.setPrefHeight(90);
conditionList.setCellFactory(lv -> new ListCell<>() {
@Override protected void updateItem(Condition c, boolean empty) {
super.updateItem(c, empty);
setText(empty || c == null ? null : ConditionDialog.describe(c));
setStyle(empty ? "" : "-fx-font-size: 11;");
}
});
Button addCondBtn = new Button("+");
Button editCondBtn = new Button("✎");
Button delCondBtn = new Button("-");
editCondBtn.setDisable(true);
delCondBtn.setDisable(true);
conditionList.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> {
boolean sel = n != null;
editCondBtn.setDisable(!sel);
delCondBtn.setDisable(!sel);
});
addCondBtn.setOnAction(e ->
new ConditionDialog().showAndWait().ifPresent(conditions::add));
editCondBtn.setOnAction(e -> {
Condition sel = conditionList.getSelectionModel().getSelectedItem();
if (sel == null) return;
new ConditionDialog(sel).showAndWait().ifPresent(updated -> {
int idx = conditions.indexOf(sel);
if (idx >= 0) conditions.set(idx, updated);
});
});
delCondBtn.setOnAction(e -> {
Condition sel = conditionList.getSelectionModel().getSelectedItem();
if (sel != null) conditions.remove(sel);
});
HBox condButtons = new HBox(4, addCondBtn, editCondBtn, delCondBtn);
condButtons.setPadding(new Insets(2, 0, 0, 0));
Label condSectionLbl = new Label("Bedingungen");
condSectionLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 12;");
VBox content = new VBox(10); VBox content = new VBox(10);
content.setPadding(new Insets(16)); content.setPadding(new Insets(16));
content.setPrefWidth(400); content.setPrefWidth(420);
content.getChildren().addAll( content.getChildren().addAll(
row("Trigger-Typ:", typeCombo), row("Trigger-Typ:", typeCombo),
row("Kapitel (mind.):", chapterSpinner), row("Kapitel (mind.):", chapterSpinner),
new Separator(), new Separator(),
dynamicArea dynamicArea,
new Separator(),
condSectionLbl,
row("Verknüpfung:", conditionModeCombo),
conditionList,
condButtons
); );
getDialogPane().setContent(content); getDialogPane().setContent(content);
@@ -90,14 +139,10 @@ public class TriggerDialog extends Dialog<Trigger> {
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK); Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
okBtn.setDisable(true); okBtn.setDisable(true);
// Validierung: OK nur wenn Typ gewählt und Pflichtfelder gefüllt
typeCombo.valueProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null)); typeCombo.valueProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null));
// Ergebnis-Konverter
setResultConverter(bt -> bt == ButtonType.OK ? buildTrigger() : null); setResultConverter(bt -> bt == ButtonType.OK ? buildTrigger() : null);
// Vorhandenen Trigger laden
if (existing != null) preload(existing); if (existing != null) preload(existing);
else typeCombo.setValue(TYPE_QUEST); else typeCombo.setValue(TYPE_QUEST);
} }
@@ -108,20 +153,18 @@ public class TriggerDialog extends Dialog<Trigger> {
dynamicArea.getChildren().clear(); dynamicArea.getChildren().clear();
if (type == null) return; if (type == null) return;
switch (type) { switch (type) {
case TYPE_QUEST -> buildQuestFields(); case TYPE_QUEST -> buildQuestFields();
case TYPE_NPC -> buildNpcFields(); case TYPE_NPC -> buildNpcFields();
case TYPE_FRACTION -> buildFractionFields(); case TYPE_FRACTION -> buildFractionFields();
case TYPE_ROUTINE -> buildRoutineFields(); case TYPE_ROUTINE -> buildRoutineFields();
case TYPE_MONOLOGUE -> buildMonologueFields(); case TYPE_MONOLOGUE -> buildMonologueFields();
case TYPE_NPC_DIALOG -> buildNpcDialogFields();
} }
} }
private void buildQuestFields() { private void buildQuestFields() {
questIdField = field("Quest-ID (z. B. q_main_001)"); questIdField = field("Quest-ID (z. B. q_main_001)");
dynamicArea.getChildren().addAll( dynamicArea.getChildren().addAll(sectionTitle("Quest"), row("Quest-ID:", questIdField));
sectionTitle("Quest"),
row("Quest-ID:", questIdField)
);
} }
private void buildNpcFields() { private void buildNpcFields() {
@@ -129,7 +172,7 @@ public class TriggerDialog extends Dialog<Trigger> {
npcStatusCombo = statusCombo(); npcStatusCombo = statusCombo();
dynamicArea.getChildren().addAll( dynamicArea.getChildren().addAll(
sectionTitle("NPC-Status"), sectionTitle("NPC-Status"),
row("NPC-ID:", npcIdField), row("NPC-ID:", npcIdField),
row("Neuer Status:", npcStatusCombo) row("Neuer Status:", npcStatusCombo)
); );
} }
@@ -139,8 +182,8 @@ public class TriggerDialog extends Dialog<Trigger> {
fractionStatusCombo = statusCombo(); fractionStatusCombo = statusCombo();
dynamicArea.getChildren().addAll( dynamicArea.getChildren().addAll(
sectionTitle("Fraktions-Status"), sectionTitle("Fraktions-Status"),
row("Fraktions-UUID:", fractionIdField), row("Fraktions-UUID:", fractionIdField),
row("Neuer Status:", fractionStatusCombo) row("Neuer Status:", fractionStatusCombo)
); );
} }
@@ -149,8 +192,8 @@ public class TriggerDialog extends Dialog<Trigger> {
routineNameField = field("Name der Routine"); routineNameField = field("Name der Routine");
dynamicArea.getChildren().addAll( dynamicArea.getChildren().addAll(
sectionTitle("Routine ändern"), sectionTitle("Routine ändern"),
row("NPC-ID:", routineNpcIdField), row("NPC-ID:", routineNpcIdField),
row("Routine-Name:", routineNameField) row("Routine-Name:", routineNameField)
); );
} }
@@ -160,14 +203,19 @@ public class TriggerDialog extends Dialog<Trigger> {
monologueIdCombo.setMaxWidth(Double.MAX_VALUE); monologueIdCombo.setMaxWidth(Double.MAX_VALUE);
monologueIdCombo.setPromptText("Monolog-ID eingeben oder wählen..."); monologueIdCombo.setPromptText("Monolog-ID eingeben oder wählen...");
try { try {
de.blight.common.MonologueIO.load().stream() MonologueIO.load().stream()
.map(Monologue::getId) .map(Monologue::getId)
.filter(id -> !id.isBlank()) .filter(id -> !id.isBlank())
.forEach(monologueIdCombo.getItems()::add); .forEach(monologueIdCombo.getItems()::add);
} catch (Exception ignored) {} } catch (Exception ignored) {}
dynamicArea.getChildren().addAll(sectionTitle("Monolog starten"), row("Monolog-ID:", monologueIdCombo));
}
private void buildNpcDialogFields() {
npcDialogIdField = field("Character-ID des NPCs");
dynamicArea.getChildren().addAll( dynamicArea.getChildren().addAll(
sectionTitle("Monolog starten"), sectionTitle("NPC-Dialog starten"),
row("Monolog-ID:", monologueIdCombo) row("NPC-ID:", npcDialogIdField)
); );
} }
@@ -188,7 +236,7 @@ public class TriggerDialog extends Dialog<Trigger> {
} }
case TYPE_NPC -> { case TYPE_NPC -> {
NpcStatusTrigger n = new NpcStatusTrigger(); NpcStatusTrigger n = new NpcStatusTrigger();
if (npcIdField != null) n.setNpcId(npcIdField.getText().trim()); if (npcIdField != null) n.setNpcId(npcIdField.getText().trim());
if (npcStatusCombo != null) n.setTargetStatus(npcStatusCombo.getValue()); if (npcStatusCombo != null) n.setTargetStatus(npcStatusCombo.getValue());
yield n; yield n;
} }
@@ -215,41 +263,56 @@ public class TriggerDialog extends Dialog<Trigger> {
} }
yield mo; yield mo;
} }
case TYPE_NPC_DIALOG -> {
NpcDialogTrigger nd = new NpcDialogTrigger();
if (npcDialogIdField != null) nd.setNpcId(npcDialogIdField.getText().trim());
yield nd;
}
default -> null; default -> null;
}; };
if (t != null) t.setRequiresChapter(chapterSpinner.getValue()); if (t == null) return null;
t.setRequiresChapter(chapterSpinner.getValue());
String modeStr = conditionModeCombo.getValue();
t.setConditionMode(modeStr != null && modeStr.startsWith("ANY") ? ConditionMode.ANY : ConditionMode.ALL);
t.setConditions(new ArrayList<>(conditions));
return t; return t;
} }
// ── Vorhandenen Trigger vorfüllen ───────────────────────────────────────── // ── Vorladen ──────────────────────────────────────────────────────────────
private void preload(Trigger t) { private void preload(Trigger t) {
chapterSpinner.getValueFactory().setValue(t.getRequiresChapter()); chapterSpinner.getValueFactory().setValue(t.getRequiresChapter());
conditionModeCombo.setValue(t.getConditionMode() == ConditionMode.ANY
? "ANY – mindestens eine" : "ALL – alle müssen gelten");
if (t.getConditions() != null) conditions.setAll(t.getConditions());
if (t instanceof QuestStartTrigger q) { if (t instanceof QuestStartTrigger q) {
typeCombo.setValue(TYPE_QUEST); typeCombo.setValue(TYPE_QUEST);
if (questIdField != null && q.getQuest() != null) if (questIdField != null && q.getQuest() != null)
questIdField.setText(nullSafe(q.getQuest().getQuestId())); questIdField.setText(nullSafe(q.getQuest().getQuestId()));
} else if (t instanceof NpcStatusTrigger n) { } else if (t instanceof NpcStatusTrigger n) {
typeCombo.setValue(TYPE_NPC); typeCombo.setValue(TYPE_NPC);
if (npcIdField != null) npcIdField.setText(nullSafe(n.getNpcId())); if (npcIdField != null) npcIdField.setText(nullSafe(n.getNpcId()));
if (npcStatusCombo != null && n.getTargetStatus() != null) if (npcStatusCombo != null && n.getTargetStatus() != null)
npcStatusCombo.setValue(n.getTargetStatus()); npcStatusCombo.setValue(n.getTargetStatus());
} else if (t instanceof FractionStatusTrigger f) { } else if (t instanceof FractionStatusTrigger f) {
typeCombo.setValue(TYPE_FRACTION); typeCombo.setValue(TYPE_FRACTION);
if (fractionIdField != null && f.getFractionId() != null) if (fractionIdField != null && f.getFractionId() != null)
fractionIdField.setText(f.getFractionId().toString()); fractionIdField.setText(f.getFractionId().toString());
if (fractionStatusCombo != null && f.getTargetStatus() != null) if (fractionStatusCombo != null && f.getTargetStatus() != null)
fractionStatusCombo.setValue(f.getTargetStatus()); fractionStatusCombo.setValue(f.getTargetStatus());
} else if (t instanceof ChangeRoutineTrigger r) { } else if (t instanceof ChangeRoutineTrigger r) {
typeCombo.setValue(TYPE_ROUTINE); typeCombo.setValue(TYPE_ROUTINE);
if (routineNpcIdField != null && r.getNpcId() != null) if (routineNpcIdField != null && r.getNpcId() != null) routineNpcIdField.setText(r.getNpcId());
routineNpcIdField.setText(r.getNpcId()); if (routineNameField != null && r.getRoutineName() != null) routineNameField.setText(r.getRoutineName());
if (routineNameField != null && r.getRoutineName() != null)
routineNameField.setText(r.getRoutineName());
} else if (t instanceof MonologueTrigger mo) { } else if (t instanceof MonologueTrigger mo) {
typeCombo.setValue(TYPE_MONOLOGUE); typeCombo.setValue(TYPE_MONOLOGUE);
if (monologueIdCombo != null && mo.getMonologueId() != null) if (monologueIdCombo != null && mo.getMonologueId() != null)
monologueIdCombo.setValue(mo.getMonologueId()); monologueIdCombo.setValue(mo.getMonologueId());
} else if (t instanceof NpcDialogTrigger nd) {
typeCombo.setValue(TYPE_NPC_DIALOG);
if (npcDialogIdField != null && nd.getNpcId() != null)
npcDialogIdField.setText(nd.getNpcId());
} }
} }

View File

@@ -111,6 +111,8 @@ public class TriggerListEditor extends VBox {
+ " -> \"" + nullSafe(r.getRoutineName()) + "\"" + chapter; + " -> \"" + nullSafe(r.getRoutineName()) + "\"" + chapter;
if (t instanceof MonologueTrigger mo) if (t instanceof MonologueTrigger mo)
return "Monolog: " + nullSafe(mo.getMonologueId()) + chapter; return "Monolog: " + nullSafe(mo.getMonologueId()) + chapter;
if (t instanceof NpcDialogTrigger nd)
return "NPC-Dialog: " + nullSafe(nd.getNpcId()) + chapter;
return t.getClass().getSimpleName() + chapter; return t.getClass().getSimpleName() + chapter;
} }

View File

@@ -222,7 +222,8 @@ public class JmeConsole extends BaseAppState {
private BitmapText makeLine(BitmapFont font, ColorRGBA color, float x, float y) { private BitmapText makeLine(BitmapFont font, ColorRGBA color, float x, float y) {
BitmapText t = new BitmapText(font); BitmapText t = new BitmapText(font);
t.setSize(LINE_H - 2f); t.setSize((LINE_H - 2f) * 2f);
t.setLocalScale(0.5f, 0.5f, 1f);
t.setColor(color); t.setColor(color);
t.setLocalTranslation(x, y, 0f); t.setLocalTranslation(x, y, 0f);
return t; return t;

View File

@@ -102,6 +102,7 @@ public class WorldScene extends BaseAppState {
private float spawnYaw = 0f; private float spawnYaw = 0f;
private de.blight.game.state.OceanSoundState oceanSound; private de.blight.game.state.OceanSoundState oceanSound;
private de.blight.game.state.AmbientSoundSystem ambientSounds; private de.blight.game.state.AmbientSoundSystem ambientSounds;
private de.blight.game.state.MusicSystem musicSystem;
private de.blight.game.audio.FootstepSystem footstepSystem; private de.blight.game.audio.FootstepSystem footstepSystem;
private de.blight.game.state.DrownState drownState; private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState; private WaterInteractionState waterInteractionState;
@@ -430,8 +431,12 @@ public class WorldScene extends BaseAppState {
app.getListener().setRotation(app.getCamera().getRotation()); app.getListener().setRotation(app.getCamera().getRotation());
if (oceanSound != null) oceanSound.setPlayerPosition(pos); if (oceanSound != null) oceanSound.setPlayerPosition(pos);
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos); if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
} }
if (musicSystem != null && dayNight != null)
musicSystem.setDaytime(dayNight.getSunElevation() >= 0f);
if (terrainMaterial != null && dayNight != null && dayNight.getSunLight() != null) { if (terrainMaterial != null && dayNight != null && dayNight.getSunLight() != null) {
terrainMaterial.setVector3("LightDir", dayNight.getSunDirection().negate()); terrainMaterial.setVector3("LightDir", dayNight.getSunDirection().negate());
ColorRGBA sc = dayNight.getSunLight().getColor(); ColorRGBA sc = dayNight.getSunLight().getColor();
@@ -945,6 +950,9 @@ public class WorldScene extends BaseAppState {
ambientSounds = new de.blight.game.state.AmbientSoundSystem(); ambientSounds = new de.blight.game.state.AmbientSoundSystem();
app.getStateManager().attach(ambientSounds); app.getStateManager().attach(ambientSounds);
musicSystem = new de.blight.game.state.MusicSystem();
app.getStateManager().attach(musicSystem);
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View File

@@ -285,6 +285,14 @@ public class DayNightState extends BaseAppState implements TimeListener {
} }
cc.a = 1f; cc.a = 1f;
cloudMat.setColor("CloudColor", cc); cloudMat.setColor("CloudColor", cc);
// Abendrot-Glut auf Wolkenunterseite (sunElevation: +1=Zenit, 0=Horizont, −1=Nadir)
// Sichtbar: Sonne bis +8.6° über Horizont (elev=+0.15) bis −5.7° darunter (Afterglow)
float glowCeil = FastMath.clamp(1f - sunElevation / 0.45f, 0f, 1f); // blendet über ~26° aus
float glowFade = FastMath.clamp((sunElevation + 0.10f) / 0.06f, 0f, 1f); // blendet unter −5.7° aus
float sunsetT = glowCeil * glowFade;
sunsetT = sunsetT * sunsetT; // weichere Kurve
cloudMat.setVector3("SunsetGlow", new Vector3f(0.42f * sunsetT, 0.13f * sunsetT, 0f));
} }
// Sonnenlicht + Schatten: Okklusion durch CloudDome bestimmt beides // Sonnenlicht + Schatten: Okklusion durch CloudDome bestimmt beides

View File

@@ -867,7 +867,8 @@ public class DialogHudState extends BaseAppState {
private BitmapText makeTxt(String s, float size, ColorRGBA color) { private BitmapText makeTxt(String s, float size, ColorRGBA color) {
BitmapText t = new BitmapText(font); BitmapText t = new BitmapText(font);
t.setSize(size); t.setSize(size * 2f);
t.setLocalScale(0.5f, 0.5f, 1f);
t.setColor(color); t.setColor(color);
t.setText(s); t.setText(s);
return t; return t;

View File

@@ -264,7 +264,8 @@ public class HotbarState extends BaseAppState {
private BitmapText txt(String s, int size, ColorRGBA col) { private BitmapText txt(String s, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font); BitmapText t = new BitmapText(font);
t.setSize(size); t.setSize(size * 2f);
t.setLocalScale(0.5f, 0.5f, 1f);
t.setColor(col); t.setColor(col);
t.setText(s); t.setText(s);
t.setQueueBucket(RenderQueue.Bucket.Gui); t.setQueueBucket(RenderQueue.Bucket.Gui);

View File

@@ -43,7 +43,8 @@ public class InteractionHudState extends BaseAppState {
BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
labelText = new BitmapText(font); labelText = new BitmapText(font);
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f); labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f);
labelText.setLocalScale(0.5f, 0.5f, 1f);
labelText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f)); labelText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f));
labelText.setCullHint(Spatial.CullHint.Always); labelText.setCullHint(Spatial.CullHint.Always);
guiNode.attachChild(labelText); guiNode.attachChild(labelText);

View File

@@ -6,63 +6,107 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.audio.AudioData; import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode; import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import de.blight.common.AreaDefinition;
import de.blight.common.AreaDefinitionIO;
import de.blight.common.AreaIO; import de.blight.common.AreaIO;
import de.blight.common.PlacedArea; import de.blight.common.PlacedArea;
import de.blight.lang.TextResolver;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Proximity-based ambient music per polygon area. * Area-based music system.
* Three parallel tracks (day / night / combat) play when the player is inside. *
* All three are attached + played on entry and detached on exit. * Track priority: combat (not yet implemented) > night > day (fallback).
* Which one is actually audible is controlled via volume (wiring to day/night deferred). * On area entry the appropriate track fades in.
* On day/night change the running track finishes its current loop,
* then the next track starts (no abrupt cut).
* Day track is always the fallback when no other track is configured.
*/ */
public class MusicSystem extends BaseAppState { public class MusicSystem extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(MusicSystem.class); private static final Logger log = LoggerFactory.getLogger(MusicSystem.class);
private static final float CHECK_INTERVAL = 0.25f; private static final float CHECK_INTERVAL = 0.25f;
private static final float FADE_DURATION = 3f; private static final float FADE_DURATION = 3f;
private static final float MUSIC_VOLUME = 0.7f; private static final float AREA_NAME_SHOW = 10f;
private static final float AREA_NAME_FADE = 1.5f;
private static final float AREA_NAME_SIZE = 24f;
private enum FadeState { INACTIVE, FADING_IN, ACTIVE, FADING_OUT } private static final int SLOT_DAY = 0;
private static final int SLOT_NIGHT = 1;
private SimpleApplication app; private enum Phase { INACTIVE, FADING_IN, ACTIVE, FINISHING, FADING_OUT }
private AssetManager assets;
private Node rootNode;
private final List<PlacedArea> data = new ArrayList<>(); private SimpleApplication app;
// three nodes per area: [0]=day, [1]=night, [2]=combat; element may be null private AssetManager assets;
private final List<AudioNode[]> tracks = new ArrayList<>(); private Node rootNode;
private final List<FadeState> fadeStates = new ArrayList<>(); private AudioSettingsState audioSettings;
private final List<PlacedArea> data = new ArrayList<>();
private final List<String> areaNameKeys = new ArrayList<>();
private final List<String> areaShortIds = new ArrayList<>();
// index [area][slot]: slot 0 = day, slot 1 = night
private final List<AudioNode[]> nodes = new ArrayList<>();
private final List<Phase> phases = new ArrayList<>();
private final List<Integer> activeSlot = new ArrayList<>();
private final List<Integer> pendingSlot = new ArrayList<>();
private Vector3f playerPos = new Vector3f(); private Vector3f playerPos = new Vector3f();
private float checkTimer = 0f; private float checkTimer = 0f;
private boolean isDaytime = true;
private BitmapText areaNameText;
private float areaNameTimer = 0f;
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override @Override
protected void initialize(Application application) { protected void initialize(Application application) {
app = (SimpleApplication) application; app = (SimpleApplication) application;
assets = app.getAssetManager(); assets = app.getAssetManager();
rootNode = app.getRootNode(); rootNode = app.getRootNode();
audioSettings = app.getStateManager().getState(AudioSettingsState.class);
BitmapFont font = assets.loadFont("Interface/Fonts/Default.fnt");
areaNameText = new BitmapText(font);
areaNameText.setSize(AREA_NAME_SIZE * 2f);
areaNameText.setLocalScale(0.5f, 0.5f, 1f);
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 0f));
app.getGuiNode().attachChild(areaNameText);
try { try {
Map<String, AreaDefinition> defs = new java.util.HashMap<>();
try {
for (AreaDefinition d : AreaDefinitionIO.load()) defs.put(d.id(), d);
} catch (IOException e) {
log.warn("[MusicSystem] Area-Definitions nicht ladbar: {}", e.getMessage());
}
for (PlacedArea area : AreaIO.load()) { for (PlacedArea area : AreaIO.load()) {
AudioNode[] arr = { AreaDefinition def = defs.get(area.areaId());
loadAmbient(area.dayTrack()), if (def == null) continue;
loadAmbient(area.nightTrack()), AudioNode day = loadTrack(def.dayTrack());
loadAmbient(area.combatTrack()) AudioNode night = loadTrack(def.nightTrack());
}; if (day == null && night == null) continue;
boolean hasAny = false; String rawId = def.id();
for (AudioNode n : arr) if (n != null) { hasAny = true; break; } String shortId = rawId.startsWith("area.") ? rawId.substring("area.".length()) : rawId;
if (!hasAny) continue;
data.add(area); data.add(area);
tracks.add(arr); areaNameKeys.add(def.nameKey());
fadeStates.add(FadeState.INACTIVE); areaShortIds.add(shortId);
nodes.add(new AudioNode[]{day, night});
phases.add(Phase.INACTIVE);
activeSlot.add(-1);
pendingSlot.add(-1);
} }
if (!data.isEmpty()) log.info("[MusicSystem] {} Musik-Bereiche geladen.", data.size()); if (!data.isEmpty()) log.info("[MusicSystem] {} Musik-Bereiche geladen.", data.size());
} catch (IOException e) { } catch (IOException e) {
@@ -72,88 +116,212 @@ public class MusicSystem extends BaseAppState {
@Override @Override
protected void cleanup(Application application) { protected void cleanup(Application application) {
for (int i = 0; i < tracks.size(); i++) { for (int i = 0; i < data.size(); i++) {
if (fadeStates.get(i) != FadeState.INACTIVE) { if (phases.get(i) != Phase.INACTIVE) stopAll(i);
for (AudioNode n : tracks.get(i)) {
if (n != null) { n.stop(); rootNode.detachChild(n); }
}
}
} }
data.clear(); data.clear();
tracks.clear(); areaNameKeys.clear();
fadeStates.clear(); areaShortIds.clear();
nodes.clear();
phases.clear();
activeSlot.clear();
pendingSlot.clear();
if (areaNameText != null) app.getGuiNode().detachChild(areaNameText);
} }
@Override protected void onEnable() {} @Override protected void onEnable() {}
@Override protected void onDisable() {} @Override protected void onDisable() {}
public void setPlayerPosition(Vector3f pos) { public void setPlayerPosition(Vector3f pos) { playerPos.set(pos); }
playerPos.set(pos);
public void setDaytime(boolean day) {
if (isDaytime == day) return;
isDaytime = day;
// trigger track switch for all active areas
for (int i = 0; i < data.size(); i++) {
Phase ph = phases.get(i);
if (ph == Phase.INACTIVE || ph == Phase.FADING_OUT) continue;
int desired = desiredSlot(i);
if (desired != activeSlot.get(i)) {
startFinishing(i, desired);
}
}
} }
// ── Update ────────────────────────────────────────────────────────────────
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (data.isEmpty()) return; // area name display timer
if (areaNameTimer > 0f) {
for (int i = 0; i < tracks.size(); i++) { areaNameTimer -= tpf;
FadeState fs = fadeStates.get(i); if (areaNameTimer <= 0f) {
if (fs == FadeState.INACTIVE || fs == FadeState.ACTIVE) continue; areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 0f));
areaNameTimer = 0f;
AudioNode[] arr = tracks.get(i); } else if (areaNameTimer < AREA_NAME_FADE) {
if (fs == FadeState.FADING_IN) { float alpha = areaNameTimer / AREA_NAME_FADE;
boolean done = true; areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, alpha));
for (AudioNode n : arr) {
if (n == null) continue;
float nv = Math.min(n.getVolume() + MUSIC_VOLUME * tpf / FADE_DURATION, MUSIC_VOLUME);
n.setVolume(nv);
if (nv < MUSIC_VOLUME) done = false;
}
if (done) fadeStates.set(i, FadeState.ACTIVE);
} else { // FADING_OUT
boolean done = true;
for (AudioNode n : arr) {
if (n == null) continue;
float nv = Math.max(n.getVolume() - MUSIC_VOLUME * tpf / FADE_DURATION, 0f);
n.setVolume(nv);
if (nv > 0f) done = false;
}
if (done) {
for (AudioNode n : arr) {
if (n != null) { n.stop(); rootNode.detachChild(n); }
}
fadeStates.set(i, FadeState.INACTIVE);
}
} }
} }
if (data.isEmpty()) return;
// per-area fade/finish handling + live volume update for active tracks
float vol = musicVol();
for (int i = 0; i < data.size(); i++) {
Phase ph = phases.get(i);
switch (ph) {
case FADING_IN -> tickFadeIn(i, tpf, vol);
case FINISHING -> tickFinishing(i);
case FADING_OUT -> tickFadeOut(i, tpf, vol);
case ACTIVE -> {
AudioNode n = activeNode(i);
if (n != null) n.setVolume(vol);
}
default -> {}
}
}
// proximity check
checkTimer += tpf; checkTimer += tpf;
if (checkTimer < CHECK_INTERVAL) return; if (checkTimer < CHECK_INTERVAL) return;
checkTimer = 0f; checkTimer = 0f;
for (int i = 0; i < data.size(); i++) { for (int i = 0; i < data.size(); i++) {
PlacedArea area = data.get(i); PlacedArea area = data.get(i);
boolean inside = pointInPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ()); boolean inside = pointInPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
FadeState fs = fadeStates.get(i); Phase ph = phases.get(i);
if (inside && (fs == FadeState.INACTIVE || fs == FadeState.FADING_OUT)) { if (inside && (ph == Phase.INACTIVE || ph == Phase.FADING_OUT)) {
if (fs == FadeState.INACTIVE) { startArea(i);
log.info("[MusicSystem] Bereich {} betreten → starte Tracks", i); } else if (!inside && (ph == Phase.ACTIVE || ph == Phase.FADING_IN || ph == Phase.FINISHING)) {
for (AudioNode n : tracks.get(i)) { beginFadeOut(i);
if (n != null) { n.setVolume(0f); rootNode.attachChild(n); n.play(); }
}
}
fadeStates.set(i, FadeState.FADING_IN);
} else if (!inside && (fs == FadeState.ACTIVE || fs == FadeState.FADING_IN)) {
log.info("[MusicSystem] Bereich {} verlassen → fade out", i);
fadeStates.set(i, FadeState.FADING_OUT);
} }
} }
} }
private AudioNode loadAmbient(String path) { // ── State transitions ─────────────────────────────────────────────────────
private void startArea(int i) {
int slot = desiredSlot(i);
AudioNode n = nodes.get(i)[slot];
if (n == null) return;
n.setVolume(0f);
n.setLooping(true);
rootNode.attachChild(n);
n.play();
activeSlot.set(i, slot);
pendingSlot.set(i, -1);
phases.set(i, Phase.FADING_IN);
log.info("[MusicSystem] Area {} → slot {} gestartet", i, slot == SLOT_DAY ? "Tag" : "Nacht");
showAreaName(i);
}
private void startFinishing(int i, int desired) {
// let current track play to end, then switch
AudioNode cur = activeNode(i);
if (cur != null) cur.setLooping(false);
pendingSlot.set(i, desired);
phases.set(i, Phase.FINISHING);
log.info("[MusicSystem] Area {} → Slot wechsel nach {}, warte auf Loop-Ende", i, desired == SLOT_DAY ? "Tag" : "Nacht");
}
private void beginFadeOut(int i) {
phases.set(i, Phase.FADING_OUT);
}
private void stopAll(int i) {
for (AudioNode n : nodes.get(i)) {
if (n != null && n.getParent() != null) { n.stop(); rootNode.detachChild(n); }
}
activeSlot.set(i, -1);
pendingSlot.set(i, -1);
phases.set(i, Phase.INACTIVE);
}
// ── Tick helpers ──────────────────────────────────────────────────────────
private void tickFadeIn(int i, float tpf, float vol) {
AudioNode n = activeNode(i);
if (n == null) { phases.set(i, Phase.ACTIVE); return; }
float nv = Math.min(n.getVolume() + vol * tpf / FADE_DURATION, vol);
n.setVolume(nv);
if (nv >= vol) phases.set(i, Phase.ACTIVE);
}
private void tickFinishing(int i) {
AudioNode cur = activeNode(i);
// wait until the current (non-looping) track finishes
if (cur != null && cur.getStatus() != AudioSource.Status.Stopped) return;
// detach old
if (cur != null && cur.getParent() != null) { cur.stop(); rootNode.detachChild(cur); }
activeSlot.set(i, -1);
int next = pendingSlot.get(i);
pendingSlot.set(i, -1);
if (next < 0 || nodes.get(i)[next] == null) {
// fallback to day
next = SLOT_DAY;
}
AudioNode n = nodes.get(i)[next];
if (n == null) { phases.set(i, Phase.INACTIVE); return; }
n.setVolume(0f);
n.setLooping(true);
rootNode.attachChild(n);
n.play();
activeSlot.set(i, next);
phases.set(i, Phase.FADING_IN);
log.info("[MusicSystem] Area {} → nächster Slot {} gestartet", i, next == SLOT_DAY ? "Tag" : "Nacht");
}
private void tickFadeOut(int i, float tpf, float vol) {
AudioNode n = activeNode(i);
if (n == null) { stopAll(i); return; }
float nv = Math.max(n.getVolume() - vol * tpf / FADE_DURATION, 0f);
n.setVolume(nv);
if (nv <= 0f) stopAll(i);
}
// ── Area-Name-Anzeige ─────────────────────────────────────────────────────
private void showAreaName(int i) {
String key = i < areaNameKeys.size() ? areaNameKeys.get(i) : "";
String shortId = i < areaShortIds.size() ? areaShortIds.get(i) : key;
String resolved = TextResolver.get().resolveOrId(key);
String name = resolved.equals(key) ? shortId : resolved;
if (name.isBlank()) return;
areaNameText.setText(name);
float tw = areaNameText.getLineWidth() * 0.5f;
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
areaNameText.setLocalTranslation((sw - tw) / 2f, sh - 40f, 0f);
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 1f));
areaNameTimer = AREA_NAME_SHOW;
}
// ── Helpers ───────────────────────────────────────────────────────────────
private float musicVol() {
return audioSettings != null ? audioSettings.effectiveMusic() : 0.7f;
}
private int desiredSlot(int i) {
AudioNode[] arr = nodes.get(i);
if (!isDaytime && arr[SLOT_NIGHT] != null) return SLOT_NIGHT;
return SLOT_DAY;
}
private AudioNode activeNode(int i) {
int slot = activeSlot.get(i);
if (slot < 0) return null;
return nodes.get(i)[slot];
}
private AudioNode loadTrack(String path) {
if (path == null || path.isEmpty()) return null; if (path == null || path.isEmpty()) return null;
try { try {
// Use Stream for music (files are large), Buffer for short sfx
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream); AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream);
n.setLooping(true); n.setLooping(true);
n.setPositional(false); n.setPositional(false);

View File

@@ -68,4 +68,23 @@ public class TextResolver {
} }
} }
} }
/** Wie resolveId(), gibt aber bei fehlendem Key den Key selbst zurück statt [key]. */
public String resolveOrId(String id) {
if (id == null || id.isBlank()) return "";
try {
return bundle.getString(id);
} catch (MissingResourceException e) {
try {
return fallback.getString(id);
} catch (MissingResourceException ex) {
return id;
}
}
}
public String resolve(TextReference ref, String fallbackText) {
if (ref == null || ref.id() == null || ref.id().isBlank()) return fallbackText;
return resolveOrId(ref.id()).equals(ref.id()) ? fallbackText : resolveOrId(ref.id());
}
} }

View File

@@ -93,4 +93,7 @@ dialog.silas.wer_seid_ihr.textnpc
erzmoss.description erzmoss.description
erzmoss.name erzmoss.name
hero.name hero.name
location.neu_1786533928723
location.neu_1786546178407
location.test
silas.name silas.name

View File

@@ -0,0 +1,2 @@
# id dayTrack nightTrack combatTrack
area.test audio/music/04-The Island.ogg audio/music/05-The Island At Night.ogg

View File

@@ -1 +1,2 @@
# polygon nameId dayTrack nightTrack combatTrack # polygon areaId
99.610,-1029.072;108.552,-1049.218;144.375,-1062.661;138.386,-1033.647;126.795,-1015.806;107.068,-1012.408 area.test

View File

@@ -1 +1,2 @@
# polygon nameId triggersJson # polygon nameId triggersJson
146.483,-1176.914;146.483,-1176.914;164.264,-1150.639;164.264,-1150.639;153.757,-1084.363;153.757,-1084.363;136.373,-1067.334;136.373,-1067.334;132.929,-1079.042;132.929,-1079.042;139.551,-1105.392;139.551,-1105.392;136.457,-1154.189;136.457,-1154.189 []

View File

@@ -1 +1,3 @@
# polygon soundPath volume crossfade # polygon soundPath volume crossfade
95.987,-1142.755;95.987,-1142.755;86.561,-1120.652;86.561,-1120.652;96.797,-1051.660;96.797,-1051.660;117.239,-1060.129;117.239,-1060.129;123.290,-1097.722;123.290,-1097.722 1.0000 false
134.038,-1118.262;134.038,-1118.262;109.293,-1093.472;109.293,-1093.472;117.239,-1060.129;117.239,-1060.129;137.925,-1072.836;137.925,-1072.836 1.0000 false