Weitere Anpassungen bezüglich der Voxel und des Zoning Systems
This commit is contained in:
@@ -4,6 +4,7 @@ MaterialDef CloudDome {
|
||||
Vector2 CloudOffset : 0.0 0.0
|
||||
Float CloudCover : 0.0
|
||||
Color CloudColor : 1.0 1.0 1.0 1.0
|
||||
Vector3 SunsetGlow : 0.0 0.0 0.0
|
||||
}
|
||||
|
||||
Technique {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
uniform vec2 m_CloudOffset;
|
||||
uniform float m_CloudCover;
|
||||
uniform vec4 m_CloudColor;
|
||||
uniform vec3 m_SunsetGlow; // additiver Rot-Orange-Ton für Wolkenunterseite bei tiefem Sonnenstand
|
||||
|
||||
in vec3 vDir;
|
||||
out vec4 outColor;
|
||||
@@ -49,5 +50,8 @@ void main() {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
blight-assets/src/main/resources/audio/music/02-The Beach.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/music/02-The Beach.ogg
Normal file
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/audio/music/04-The Island.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/music/04-The Island.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/audio/music/20-Showdown.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/music/20-Showdown.ogg
Normal file
Binary file not shown.
@@ -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"; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.blight.common;
|
||||
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
import de.blight.common.model.trigger.TriggerIO;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
@@ -16,18 +19,14 @@ public final class AreaIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# polygon\tnameId\tdayTrack\tnightTrack\tcombatTrack");
|
||||
w.write("# polygon\tareaId\ttriggersJson");
|
||||
w.newLine();
|
||||
for (PlacedArea a : areas) {
|
||||
w.write(SoundAreaIO.encodePolygon(a.pointsX(), a.pointsZ()));
|
||||
w.write('\t');
|
||||
w.write(a.nameId());
|
||||
w.write(a.areaId());
|
||||
w.write('\t');
|
||||
w.write(a.dayTrack());
|
||||
w.write('\t');
|
||||
w.write(a.nightTrack());
|
||||
w.write('\t');
|
||||
w.write(a.combatTrack());
|
||||
w.write(TriggerIO.serializeList(a.triggers()));
|
||||
w.newLine();
|
||||
}
|
||||
}
|
||||
@@ -38,14 +37,16 @@ public final class AreaIO {
|
||||
if (!Files.exists(p)) return List.of();
|
||||
List<PlacedArea> list = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(p)) {
|
||||
line = line.strip();
|
||||
line = line.stripLeading();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
String[] f = line.split("\t", -1);
|
||||
if (f.length < 5) continue;
|
||||
if (f.length < 1) continue;
|
||||
try {
|
||||
float[][] pts = SoundAreaIO.decodePolygon(f[0]);
|
||||
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) {}
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package de.blight.common;
|
||||
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record PlacedArea(
|
||||
float[] pointsX,
|
||||
float[] pointsZ,
|
||||
String nameId,
|
||||
String dayTrack,
|
||||
String nightTrack,
|
||||
String combatTrack
|
||||
) {}
|
||||
String areaId,
|
||||
List<Trigger> triggers
|
||||
) {
|
||||
public PlacedArea(float[] pointsX, float[] pointsZ, String areaId) {
|
||||
this(pointsX, pointsZ, areaId, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,6 @@ public class Location {
|
||||
public void entered(MainCharacter character) {
|
||||
triggers.stream()
|
||||
.filter(t -> t.isTriggarable(character))
|
||||
.forEach(t -> t.trigger(character));
|
||||
.forEach(t -> t.fire(character));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package de.blight.common.model;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.blight.common.model.quests.Quest;
|
||||
import lombok.AccessLevel;
|
||||
@@ -44,6 +47,18 @@ public class MainCharacter extends GameCharacter {
|
||||
/** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */
|
||||
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)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private List<CharacterListener> listeners = new ArrayList<CharacterListener>();
|
||||
@@ -53,6 +68,11 @@ public class MainCharacter extends GameCharacter {
|
||||
@Setter(AccessLevel.NONE)
|
||||
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) {
|
||||
if (option.getRequiredItem() != null) {
|
||||
getInventar().remove(option.getRequiredItem());
|
||||
@@ -146,6 +166,37 @@ public class MainCharacter extends GameCharacter {
|
||||
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. */
|
||||
public void handleMonologue(Monologue m) {
|
||||
if (m.getRecievesQuest() != null) startQuest(m.getRecievesQuest());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.blight.common.model.trigger;
|
||||
|
||||
public enum ConditionMode { ALL, ANY }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,35 @@ import de.blight.common.model.MainCharacter;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public abstract class Trigger {
|
||||
|
||||
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) {
|
||||
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 final void fire(MainCharacter character) {
|
||||
trigger(character);
|
||||
if (triggerId != null) character.getFiredTriggerIds().add(triggerId);
|
||||
}
|
||||
|
||||
public abstract boolean isTriggarableDelegate(MainCharacter character);
|
||||
|
||||
@@ -9,19 +9,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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 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_CHANGE_ROUTINE = "CHANGE_ROUTINE";
|
||||
public static final String TYPE_MONOLOGUE = "MONOLOGUE";
|
||||
public static final String TYPE_NPC_DIALOG = "NPC_DIALOG";
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter())
|
||||
@@ -36,18 +24,15 @@ public final class TriggerIO {
|
||||
|
||||
private TriggerIO() {}
|
||||
|
||||
/** Registriert den Trigger-Typ-Adapter an einem GsonBuilder (für andere Module). */
|
||||
public static GsonBuilder registerAdapters(GsonBuilder builder) {
|
||||
return builder.registerTypeHierarchyAdapter(Trigger.class, new TriggerAdapter());
|
||||
}
|
||||
|
||||
/** Serialisiert eine Trigger-Liste als kompaktes JSON (kein Zeilenumbruch). */
|
||||
public static String serializeList(List<Trigger> triggers) {
|
||||
if (triggers == null || triggers.isEmpty()) return "[]";
|
||||
return GSON.toJson(triggers);
|
||||
}
|
||||
|
||||
/** Deserialisiert eine Trigger-Liste aus JSON. Gibt leere Liste bei Fehler zurück. */
|
||||
public static List<Trigger> deserializeList(String json) {
|
||||
if (json == null || json.isBlank() || "[]".equals(json.strip())) return new ArrayList<>();
|
||||
try {
|
||||
@@ -71,7 +56,13 @@ public final class TriggerIO {
|
||||
public JsonElement serialize(Trigger src, Type typeOfSrc, JsonSerializationContext ctx) {
|
||||
JsonObject obj = new JsonObject();
|
||||
obj.addProperty("type", typeOf(src));
|
||||
if (src.getTriggerId() != null) obj.addProperty("triggerId", src.getTriggerId());
|
||||
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 (q.getQuest() != null && q.getQuest().getQuestId() != null)
|
||||
@@ -90,6 +81,8 @@ public final class TriggerIO {
|
||||
if (r.getRoutineName() != null) obj.addProperty("routineName", r.getRoutineName());
|
||||
} else if (src instanceof MonologueTrigger mo) {
|
||||
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;
|
||||
}
|
||||
@@ -136,20 +129,117 @@ public final class TriggerIO {
|
||||
if (obj.has("monologueId")) mo.setMonologueId(obj.get("monologueId").getAsString());
|
||||
yield mo;
|
||||
}
|
||||
case TYPE_NPC_DIALOG -> {
|
||||
NpcDialogTrigger nd = new NpcDialogTrigger();
|
||||
if (obj.has("npcId")) nd.setNpcId(obj.get("npcId").getAsString());
|
||||
yield nd;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (t != null && obj.has("requiresChapter"))
|
||||
t.setRequiresChapter(obj.get("requiresChapter").getAsInt());
|
||||
if (t == null) return null;
|
||||
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;
|
||||
}
|
||||
|
||||
// ── 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) {
|
||||
if (t instanceof QuestStartTrigger) return TYPE_QUEST_START;
|
||||
if (t instanceof NpcStatusTrigger) return TYPE_NPC_STATUS;
|
||||
if (t instanceof FractionStatusTrigger) return TYPE_FRACTION_STATUS;
|
||||
if (t instanceof ChangeRoutineTrigger) return TYPE_CHANGE_ROUTINE;
|
||||
if (t instanceof MonologueTrigger) return TYPE_MONOLOGUE;
|
||||
if (t instanceof NpcDialogTrigger) return TYPE_NPC_DIALOG;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ plugins {
|
||||
|
||||
javafx {
|
||||
version = '26'
|
||||
modules = ['javafx.controls', 'javafx.swing']
|
||||
modules = ['javafx.controls', 'javafx.swing', 'javafx.media']
|
||||
}
|
||||
|
||||
application {
|
||||
|
||||
@@ -180,6 +180,7 @@ public class AudioPreviewPopup {
|
||||
private void buildStage() {
|
||||
stage = new Stage(StageStyle.UTILITY);
|
||||
stage.setTitle("Audio-Vorschau");
|
||||
stage.initOwner(de.blight.editor.ui.Dialogs.primaryWindow());
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setResizable(false);
|
||||
stage.setOnCloseRequest(e -> stopClip());
|
||||
|
||||
@@ -240,6 +240,10 @@ public class EditorApp extends Application {
|
||||
// Location-Zonen-Werkzeug-Zustand
|
||||
private VBox locationZoneDynamicContent;
|
||||
|
||||
// Zonen-Werkzeug-Zustand (vereint Sound, Area, Location)
|
||||
private VBox zonenDynamicContent;
|
||||
private Runnable zonenKindStyleUpdater;
|
||||
|
||||
// Spiel-Starten-Werkzeug-Zustand
|
||||
private TextField spawnXField;
|
||||
private TextField spawnZField;
|
||||
@@ -324,9 +328,7 @@ public class EditorApp extends Application {
|
||||
private ToggleButton emitterBtn;
|
||||
private ToggleButton waterBtn;
|
||||
private ToggleButton riverBtn;
|
||||
private ToggleButton soundAreaBtn;
|
||||
private ToggleButton areaBtn;
|
||||
private ToggleButton locationZoneBtn;
|
||||
private ToggleButton zonenBtn;
|
||||
private ToggleButton playToolBtn;
|
||||
private ToggleButton voxelBtn;
|
||||
private ToggleButton voxelCliffBtn;
|
||||
@@ -350,6 +352,8 @@ public class EditorApp extends Application {
|
||||
private double editPressX, editPressY;
|
||||
private int editPressAction;
|
||||
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)
|
||||
private TreeItem<String> assetTreeRoot;
|
||||
@@ -672,7 +676,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.areaOverlapRejected) {
|
||||
input.areaOverlapRejected = false;
|
||||
setStatus("Bereich abgelehnt: überschneidet einen bestehenden Bereich.");
|
||||
setStatus("Area abgelehnt: überschneidet eine bestehende Area.");
|
||||
}
|
||||
|
||||
if (input.locationZoneSelectionChanged) {
|
||||
@@ -680,6 +684,12 @@ public class EditorApp extends Application {
|
||||
updateLocationZonePanel(input.selectedLocationZoneInfo);
|
||||
}
|
||||
|
||||
if (input.zonenSelectionChanged) {
|
||||
input.zonenSelectionChanged = false;
|
||||
updateZonenPanel(input.zonenSelectedKind);
|
||||
if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run();
|
||||
}
|
||||
|
||||
if (input.cliffZoneSelectionChanged) {
|
||||
input.cliffZoneSelectionChanged = false;
|
||||
updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0);
|
||||
@@ -852,16 +862,12 @@ public class EditorApp extends Application {
|
||||
statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE);
|
||||
statusPoller.play();
|
||||
|
||||
javafx.animation.Timeline autoSave = new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(javafx.util.Duration.seconds(60), ev -> {
|
||||
worldAutoSave.setOnFinished(ev -> {
|
||||
if (!input.saveRequested) {
|
||||
input.saveRequested = true;
|
||||
setStatus("Auto-Speicherung…");
|
||||
}
|
||||
})
|
||||
);
|
||||
autoSave.setCycleCount(javafx.animation.Timeline.INDEFINITE);
|
||||
autoSave.play();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Modus-Wechsel ────────────────────────────────────────────────────────
|
||||
@@ -1345,6 +1351,7 @@ public class EditorApp extends Application {
|
||||
MenuItem ctEditItem = new MenuItem("Crafting-Table-Manager");
|
||||
MenuItem fractionEditItem = new MenuItem("Fraktionen-Manager");
|
||||
MenuItem locationEditItem = new MenuItem("Locations-Manager");
|
||||
MenuItem areaEditItem = new MenuItem("Areas-Manager");
|
||||
MenuItem localizationEditItem = new MenuItem("Lokalisierungs-Editor");
|
||||
vegetationsItem.setOnAction(e -> switchToVegetationGenerator());
|
||||
ezTreeItem.setOnAction(e -> switchToEzTree());
|
||||
@@ -1359,11 +1366,12 @@ public class EditorApp extends Application {
|
||||
ctEditItem.setOnAction(e -> switchToCraftingTableEditor());
|
||||
fractionEditItem.setOnAction(e -> switchToFractionEditor());
|
||||
locationEditItem.setOnAction(e -> switchToLocationEditor());
|
||||
areaEditItem.setOnAction(e -> switchToAreaEditor());
|
||||
localizationEditItem.setOnAction(e -> switchToLocalizationEditor());
|
||||
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
|
||||
animPrevItem, objEditorItem, worldEditItem, charEditItem,
|
||||
questEditItem, itemEditItem, recipeEditItem, ctEditItem, fractionEditItem,
|
||||
locationEditItem, localizationEditItem);
|
||||
locationEditItem, areaEditItem, localizationEditItem);
|
||||
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
@@ -1425,9 +1433,7 @@ public class EditorApp extends Application {
|
||||
emitterBtn = new ToggleButton("🔥 Emitter");
|
||||
waterBtn = new ToggleButton("💧 Wasser");
|
||||
riverBtn = new ToggleButton("↯ Wasserfall");
|
||||
soundAreaBtn = new ToggleButton("🔊 Sound");
|
||||
areaBtn = new ToggleButton("🗺 Bereiche");
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
zonenBtn = new ToggleButton("🗺 Zonen");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
voxelCliffBtn = new ToggleButton("⛰ Klippe");
|
||||
@@ -1442,9 +1448,7 @@ public class EditorApp extends Application {
|
||||
emitterBtn.setStyle("-fx-font-weight:bold;");
|
||||
waterBtn.setStyle("-fx-font-weight:bold;");
|
||||
riverBtn.setStyle("-fx-font-weight:bold;");
|
||||
soundAreaBtn.setStyle("-fx-font-weight:bold;");
|
||||
areaBtn.setStyle("-fx-font-weight:bold;");
|
||||
locationZoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
zonenBtn.setStyle("-fx-font-weight:bold;");
|
||||
playToolBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelCliffBtn.setStyle("-fx-font-weight:bold;");
|
||||
@@ -1461,9 +1465,7 @@ public class EditorApp extends Application {
|
||||
emitterBtn.setToggleGroup(layerGroup);
|
||||
waterBtn.setToggleGroup(layerGroup);
|
||||
riverBtn.setToggleGroup(layerGroup);
|
||||
soundAreaBtn.setToggleGroup(layerGroup);
|
||||
areaBtn.setToggleGroup(layerGroup);
|
||||
locationZoneBtn.setToggleGroup(layerGroup);
|
||||
zonenBtn.setToggleGroup(layerGroup);
|
||||
playToolBtn.setToggleGroup(layerGroup);
|
||||
voxelBtn.setToggleGroup(layerGroup);
|
||||
voxelCliffBtn.setToggleGroup(layerGroup);
|
||||
@@ -1523,17 +1525,9 @@ public class EditorApp extends Application {
|
||||
input.activeLayer = SharedInput.LAYER_WATERFALL;
|
||||
root.setRight(buildWaterfallPanel());
|
||||
});
|
||||
soundAreaBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_SOUND_AREAS;
|
||||
root.setRight(buildSoundAreaPanel());
|
||||
});
|
||||
areaBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_AREAS;
|
||||
root.setRight(buildAreaPanel());
|
||||
});
|
||||
locationZoneBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_LOCATION_ZONES;
|
||||
root.setRight(buildLocationZonePanel());
|
||||
zonenBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_ZONEN;
|
||||
root.setRight(buildZonenPanel());
|
||||
});
|
||||
playToolBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
|
||||
@@ -1574,7 +1568,7 @@ public class EditorApp extends Application {
|
||||
new Separator(Orientation.VERTICAL), emitterBtn,
|
||||
new Separator(Orientation.VERTICAL), waterBtn,
|
||||
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), voxelBtn, voxelCliffBtn,
|
||||
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 "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); }
|
||||
case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; root.setRight(buildWaterPanel()); }
|
||||
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_SOUND_AREAS; root.setRight(buildSoundAreaPanel()); }
|
||||
case "area" -> { input.activeLayer = SharedInput.LAYER_AREAS; root.setRight(buildAreaPanel()); }
|
||||
case "locationzone" -> { input.activeLayer = SharedInput.LAYER_LOCATION_ZONES; root.setRight(buildLocationZonePanel()); }
|
||||
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "sound"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
|
||||
case "area" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "area"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
|
||||
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()); }
|
||||
}
|
||||
},
|
||||
@@ -4707,6 +4701,7 @@ public class EditorApp extends Application {
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
}
|
||||
scheduleAutoSave();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7857,19 +7852,30 @@ public class EditorApp extends Application {
|
||||
} else if (bothDown) {
|
||||
stopEditTimer();
|
||||
} 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) {
|
||||
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
|
||||
input.playToolClickQueue.offer(
|
||||
new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY()));
|
||||
} else if (singleClickLayer) {
|
||||
submitEdit(e.getX(), e.getY(), +1);
|
||||
} else {
|
||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
startEditTimer();
|
||||
}
|
||||
} 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;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
startEditTimer();
|
||||
if (!singleClickLayer) startEditTimer();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7969,6 +7975,8 @@ public class EditorApp extends Application {
|
||||
if (editTimer != null) { editTimer.stop(); editTimer = null; }
|
||||
}
|
||||
|
||||
private void scheduleAutoSave() { worldAutoSave.playFromStart(); }
|
||||
|
||||
private void submitEdit(double x, double y, int action) {
|
||||
switch (input.activeLayer) {
|
||||
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));
|
||||
case SharedInput.LAYER_LOCATION_ZONES ->
|
||||
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 ->
|
||||
input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_PLAY_TOOL -> {
|
||||
@@ -8008,6 +8021,7 @@ public class EditorApp extends Application {
|
||||
new SharedInput.ModelInteractableClick((float) x, (float) y));
|
||||
}
|
||||
}
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
// ── Statusleiste ─────────────────────────────────────────────────────────
|
||||
@@ -8328,6 +8342,7 @@ public class EditorApp extends Application {
|
||||
if (pressed && (input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|
||||
|| input.activeLayer == SharedInput.LAYER_ZONEN
|
||||
|| input.activeLayer == SharedInput.LAYER_WATER
|
||||
|| input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF))
|
||||
input.cancelZoneDrawing = true;
|
||||
@@ -8337,12 +8352,17 @@ public class EditorApp extends Application {
|
||||
if (input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
|
||||
input.deleteSelectedRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS)
|
||||
input.deleteSoundAreaRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_AREAS)
|
||||
input.deleteAreaRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES)
|
||||
input.deleteLocationZoneRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
||||
input.deleteSoundAreaRequested = true; scheduleAutoSave();
|
||||
} else if (input.activeLayer == SharedInput.LAYER_AREAS) {
|
||||
input.deleteAreaRequested = true; scheduleAutoSave();
|
||||
} else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) {
|
||||
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)
|
||||
input.deleteVoxelCliffZoneRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_WATER)
|
||||
@@ -8509,6 +8529,115 @@ public class EditorApp extends Application {
|
||||
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() {
|
||||
VBox inner = new VBox(8);
|
||||
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.setWrapText(true);
|
||||
|
||||
Button browseBtn = new Button("📂 Datei wählen…");
|
||||
Button browseBtn = new Button("🔊 Sound wählen…");
|
||||
browseBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
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");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
File chosen = fc.showOpenDialog(primaryStage);
|
||||
if (chosen != null) {
|
||||
try {
|
||||
String rel = ensureOgg(chosen, assetRoot);
|
||||
new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL)
|
||||
.showAndWait()
|
||||
.ifPresent(rel -> {
|
||||
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");
|
||||
@@ -8639,6 +8758,7 @@ public class EditorApp extends Application {
|
||||
// pendingSoundArea != null and idx matches to update only the props.
|
||||
input.pendingSoundArea.set(new de.blight.common.PlacedSoundArea(
|
||||
new float[0], new float[0], soundPath, volume, crossfade));
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
// ── Musik-Bereich-Panel ───────────────────────────────────────────────────
|
||||
@@ -8647,18 +8767,18 @@ public class EditorApp extends Application {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
inner.getChildren().addAll(
|
||||
sectionTitle("Bereiche"),
|
||||
sectionTitle("Areas"),
|
||||
styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||
styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
|
||||
styledHint("ESC → Zeichnen abbrechen"),
|
||||
styledHint("Entf → Bereich löschen"),
|
||||
styledHint("Bereiche dürfen sich nicht überschneiden"),
|
||||
styledHint("Entf → Area löschen"),
|
||||
styledHint("Areas dürfen sich nicht überschneiden"),
|
||||
new Separator(),
|
||||
sectionTitle("Gewählter Bereich"),
|
||||
sectionTitle("Gewählte Area"),
|
||||
new Separator());
|
||||
|
||||
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;");
|
||||
areaDynamicContent.getChildren().add(noSel);
|
||||
inner.getChildren().add(areaDynamicContent);
|
||||
@@ -8679,71 +8799,80 @@ public class EditorApp extends Application {
|
||||
areaDynamicContent.getChildren().clear();
|
||||
|
||||
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;");
|
||||
areaDynamicContent.getChildren().add(noSel);
|
||||
return;
|
||||
}
|
||||
// Format: "idx|nameId|dayTrack|nightTrack|combatTrack"
|
||||
String[] p = info.split("\\|", -1);
|
||||
if (p.length < 5) return;
|
||||
// Format: "idx|areaId|triggersJson"
|
||||
String[] p = info.split("\\|", 3);
|
||||
if (p.length < 1) return;
|
||||
try {
|
||||
int idx = Integer.parseInt(p[0]);
|
||||
final String[] nameRef = {p[1]};
|
||||
final String[] tracks = {p[2], p[3], p[4]};
|
||||
String[] trackLabels = {"☀ Tag-Track", "🌙 Nacht-Track", "⚔ Kampf-Track"};
|
||||
String currentAreaId = p.length > 1 ? p[1] : "";
|
||||
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
|
||||
p.length > 2
|
||||
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
|
||||
: new java.util.ArrayList<>();
|
||||
|
||||
Runnable publish = () ->
|
||||
input.pendingArea.set(new de.blight.common.PlacedArea(
|
||||
new float[0], new float[0], nameRef[0], tracks[0], tracks[1], tracks[2]));
|
||||
|
||||
// Name (TextReference-ID)
|
||||
Label nameLbl = new Label("Name (TextReference):");
|
||||
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++) {
|
||||
final int tIdx = ti;
|
||||
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…");
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
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) {
|
||||
// Verfügbare Area-Definitionen laden
|
||||
java.util.List<String> areaIds = new java.util.ArrayList<>();
|
||||
areaIds.add("");
|
||||
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());
|
||||
}
|
||||
}
|
||||
de.blight.common.AreaDefinitionIO.load().stream()
|
||||
.map(de.blight.common.AreaDefinition::id)
|
||||
.filter(s -> !s.isBlank())
|
||||
.forEach(areaIds::add);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
Label areaLbl = new Label("Verknüpfte Area:");
|
||||
areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
|
||||
Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name");
|
||||
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
|
||||
|
||||
// editorHolder muss vor areaCombo deklariert sein (Lambda-Forward-Ref)
|
||||
de.blight.editor.ui.TriggerListEditor[] editorHolder = {null};
|
||||
|
||||
ComboBox<String> areaCombo = new ComboBox<>();
|
||||
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();
|
||||
});
|
||||
|
||||
areaDynamicContent.getChildren().addAll(lbl, btn);
|
||||
if (ti < 2) areaDynamicContent.getChildren().add(new Separator());
|
||||
}
|
||||
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");
|
||||
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||
delBtn.setOnAction(e -> input.deleteAreaRequested = true);
|
||||
areaDynamicContent.getChildren().addAll(new Separator(), delBtn);
|
||||
delBtn.setOnAction(e -> { input.deleteAreaRequested = true; scheduleAutoSave(); });
|
||||
|
||||
areaDynamicContent.getChildren().addAll(
|
||||
areaLbl, areaCombo, nameKeyLbl,
|
||||
new Separator(),
|
||||
triggerLbl, editorHolder[0],
|
||||
new Separator(),
|
||||
delBtn);
|
||||
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
@@ -8795,14 +8924,11 @@ public class EditorApp extends Application {
|
||||
try {
|
||||
int idx = Integer.parseInt(p[0]);
|
||||
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
|
||||
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
|
||||
: 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
|
||||
java.util.List<String> locationIds = new java.util.ArrayList<>();
|
||||
locationIds.add("");
|
||||
@@ -8815,44 +8941,30 @@ public class EditorApp extends Application {
|
||||
|
||||
Label nameLbl = new Label("Verknüpfte Location:");
|
||||
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<>();
|
||||
locationCombo.getItems().setAll(locationIds);
|
||||
locationCombo.setValue(locationIds.contains(nameId) ? nameId : "");
|
||||
locationCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
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) -> {
|
||||
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(
|
||||
new float[0], new float[0],
|
||||
n != null && !n.isBlank() ? n : nameTf.getText(),
|
||||
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers));
|
||||
new float[0], new float[0], lid, existingTriggers));
|
||||
scheduleAutoSave();
|
||||
});
|
||||
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");
|
||||
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||
delBtn.setOnAction(e -> input.deleteLocationZoneRequested = true);
|
||||
delBtn.setOnAction(e -> { input.deleteLocationZoneRequested = true; scheduleAutoSave(); });
|
||||
|
||||
locationZoneDynamicContent.getChildren().addAll(
|
||||
nameLbl, locationCombo, nameTf,
|
||||
new Separator(),
|
||||
triggerLbl, triggerEditor,
|
||||
nameLbl, locationCombo, nameKeyLbl,
|
||||
new Separator(),
|
||||
delBtn);
|
||||
|
||||
@@ -11239,6 +11351,20 @@ public class EditorApp extends Application {
|
||||
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() {
|
||||
onF5 = null;
|
||||
currentTool = "localizationEditor";
|
||||
|
||||
@@ -359,6 +359,8 @@ public class SharedInput {
|
||||
public volatile boolean reloadPlacedModels = false;
|
||||
public volatile boolean reloadPlacedItems = false;
|
||||
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). */
|
||||
public volatile float camYaw = 0f;
|
||||
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
||||
@@ -546,6 +548,18 @@ public class SharedInput {
|
||||
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
|
||||
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 ────────────────────────────────────────────────
|
||||
/** Klick/Drag-Ereignisse im Viewport für das Play-Tool. */
|
||||
public record PlayToolClick(float screenX, float screenY) {}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.PlacedArea;
|
||||
import de.blight.common.model.trigger.TriggerIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
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 ColorRGBA COLOR_NORMAL = new ColorRGBA(0.5f, 0.3f, 1f, 1f);
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.8f, 0.5f, 1f, 1f);
|
||||
private static final ColorRGBA COLOR_OVERLAP = new ColorRGBA(1f, 0.2f, 0.2f, 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(1.00f, 0.55f, 0.55f, 1f); // Hell-Rot
|
||||
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(1.00f, 0.85f, 0.00f, 1f); // Gelb (Fehler)
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -79,7 +80,7 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -94,9 +95,9 @@ public class AreaState extends BaseAppState {
|
||||
applyProperty(selectedIdx, pending);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
if (input.cancelZoneDrawing && placing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) cancelPoly();
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
if (input.deleteAreaRequested) {
|
||||
@@ -116,16 +117,13 @@ public class AreaState extends BaseAppState {
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) closePoly();
|
||||
if (placing) undoLastPoint();
|
||||
else deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
@@ -133,10 +131,11 @@ public class AreaState extends BaseAppState {
|
||||
hitX = snapped[0];
|
||||
hitZ = snapped[1];
|
||||
|
||||
// auto-close only when snapped exactly to own first vertex
|
||||
if (currX.size() >= 3) {
|
||||
float dx = hitX - currX.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();
|
||||
return;
|
||||
}
|
||||
@@ -146,19 +145,41 @@ public class AreaState extends BaseAppState {
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} 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++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||
selectArea(i);
|
||||
return;
|
||||
selectArea(i); 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();
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"area".equals(input.zonenNewKind)) return;
|
||||
placing = true;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
currX.add(hitX);
|
||||
currZ.add(hitZ);
|
||||
// snap first point to nearby existing vertices
|
||||
float[] startSnap = snapVertex(hitX, hitZ);
|
||||
currX.add(startSnap[0]);
|
||||
currZ.add(startSnap[1]);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
}
|
||||
@@ -195,9 +216,10 @@ public class AreaState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
PlacedArea area = new PlacedArea(xs, zs, "", "", "", "");
|
||||
PlacedArea area = new PlacedArea(xs, zs, "");
|
||||
addArea(area);
|
||||
selectArea(areas.size() - 1);
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
@@ -209,6 +231,14 @@ public class AreaState extends BaseAppState {
|
||||
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() {
|
||||
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||
int n = currX.size();
|
||||
@@ -224,7 +254,7 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
float x = currX.get(currX.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;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
@@ -251,25 +281,42 @@ public class AreaState extends BaseAppState {
|
||||
private void selectArea(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
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;
|
||||
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
areaGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(8f);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void publishSelection(int 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;
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
input.zonenSelectedKind = "area";
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||
@@ -303,9 +350,9 @@ public class AreaState extends BaseAppState {
|
||||
private void applyProperty(int idx, PlacedArea updated) {
|
||||
if (updated.pointsX().length == 0) {
|
||||
PlacedArea existing = areas.get(idx);
|
||||
areas.set(idx, new PlacedArea(
|
||||
existing.pointsX(), existing.pointsZ(),
|
||||
updated.nameId(), updated.dayTrack(), updated.nightTrack(), updated.combatTrack()));
|
||||
areas.set(idx, new PlacedArea(existing.pointsX(), existing.pointsZ(),
|
||||
updated.areaId(),
|
||||
updated.triggers() != null ? updated.triggers() : existing.triggers()));
|
||||
} else {
|
||||
areas.set(idx, updated);
|
||||
}
|
||||
@@ -317,7 +364,7 @@ public class AreaState extends BaseAppState {
|
||||
int n = xs.size();
|
||||
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
|
||||
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.flip();
|
||||
@@ -329,13 +376,29 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
private boolean overlapsExistingAreas(float[] xs, float[] zs) {
|
||||
@@ -383,6 +446,15 @@ public class AreaState extends BaseAppState {
|
||||
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) {
|
||||
if (rootNode == null) {
|
||||
pendingAreas = new ArrayList<>(loaded);
|
||||
@@ -405,4 +477,66 @@ public class AreaState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +119,8 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
if (click.rightButton()) { deselect(); return; }
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
|
||||
PlacedEmitter pe = createPreset(input.emitterPreset, pt.x, pt.y, pt.z);
|
||||
addEmitter(pe);
|
||||
@@ -327,4 +324,34 @@ public class EmitterState extends BaseAppState {
|
||||
clearAll();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +129,8 @@ public class LightState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Neues Licht auf dem Terrain platzieren
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
|
||||
PlacedLight pl = new PlacedLight(pt.x, pt.y + 1f, pt.z, 1f, 1f, 1f, 1f, 20f);
|
||||
addLight(pl);
|
||||
@@ -317,4 +313,34 @@ public class LightState extends BaseAppState {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
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_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.5f, 1f, 0.6f, 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(1.00f, 1.00f, 0.55f, 1f); // Hell-Gelb
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.95f, 0.30f, 1f); // Mittel-Gelb
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -78,7 +78,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -93,9 +93,9 @@ public class LocationZoneState extends BaseAppState {
|
||||
applyProperty(selectedIdx, pending);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
if (input.cancelZoneDrawing && placing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) cancelPoly();
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
if (input.deleteLocationZoneRequested) {
|
||||
@@ -115,16 +115,13 @@ public class LocationZoneState extends BaseAppState {
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) closePoly();
|
||||
if (placing) undoLastPoint();
|
||||
else deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
@@ -132,10 +129,11 @@ public class LocationZoneState extends BaseAppState {
|
||||
hitX = snapped[0];
|
||||
hitZ = snapped[1];
|
||||
|
||||
// auto-close only when snapped exactly to own first vertex
|
||||
if (currX.size() >= 3) {
|
||||
float dx = hitX - currX.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();
|
||||
return;
|
||||
}
|
||||
@@ -145,18 +143,40 @@ public class LocationZoneState extends BaseAppState {
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} 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++) {
|
||||
PlacedLocationZone z = zones.get(i);
|
||||
if (SoundAreaState.pointInPolygon(hitX, hitZ, z.pointsX(), z.pointsZ())) {
|
||||
selectZone(i);
|
||||
return;
|
||||
selectZone(i); 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();
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"location".equals(input.zonenNewKind)) return;
|
||||
placing = true;
|
||||
currX.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);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
@@ -189,6 +209,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
PlacedLocationZone zone = new PlacedLocationZone(xs, zs, "");
|
||||
addZone(zone);
|
||||
selectZone(zones.size() - 1);
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
@@ -200,6 +221,14 @@ public class LocationZoneState extends BaseAppState {
|
||||
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() {
|
||||
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||
int n = currX.size();
|
||||
@@ -215,7 +244,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
float x = currX.get(currX.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;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
@@ -242,19 +271,31 @@ public class LocationZoneState extends BaseAppState {
|
||||
private void selectZone(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
|
||||
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;
|
||||
zoneGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
zoneGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(10f);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
input.selectedLocationZoneInfo = idx + "|" + z.nameId() + "|" + triggersJson;
|
||||
input.locationZoneSelectionChanged = true;
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
input.zonenSelectedKind = "location";
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||
@@ -310,7 +355,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
int n = xs.size();
|
||||
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
|
||||
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.flip();
|
||||
@@ -322,19 +367,44 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||
mat.getAdditionalRenderState().setLineWidth(6f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
public List<PlacedLocationZone> getPlacedZones() {
|
||||
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) {
|
||||
if (rootNode == null) {
|
||||
pendingZones = new ArrayList<>(loaded);
|
||||
@@ -357,4 +427,66 @@ public class LocationZoneState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,7 +859,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
al.r(), al.g(), al.b(), al.intensity(), al.radius()));
|
||||
}
|
||||
de.blight.common.LightIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
input.reloadLightsEmitters = true;
|
||||
} catch (java.io.IOException e) {
|
||||
log.error("[SceneObject] Anhang-Licht speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
@@ -883,7 +883,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
list.add(pe);
|
||||
}
|
||||
de.blight.common.EmitterIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
input.reloadLightsEmitters = true;
|
||||
} catch (java.io.IOException e) {
|
||||
log.error("[SceneObject] Anhang-Emitter speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@ public class SculptedMeshEditorState extends BaseAppState {
|
||||
|
||||
/** 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) {
|
||||
if (sculptRoot == null) return null;
|
||||
CollisionResults cr = new CollisionResults();
|
||||
sculptRoot.collideWith(ray, cr);
|
||||
return cr.size() > 0 ? cr.getClosestCollision().getContactPoint() : null;
|
||||
|
||||
@@ -26,9 +26,9 @@ public class SoundAreaState extends BaseAppState {
|
||||
static final float SNAP_DIST = 8f;
|
||||
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_SELECTED = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.3f, 0.9f, 1f, 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(1.00f, 0.75f, 0.45f, 1f); // Hell-Orange
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.65f, 0.20f, 1f); // Mittel-Orange
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -82,7 +82,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@@ -97,9 +97,9 @@ public class SoundAreaState extends BaseAppState {
|
||||
applyProperty(selectedIdx, pending);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
if (input.cancelZoneDrawing && placing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) cancelPoly();
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
if (input.deleteSoundAreaRequested) {
|
||||
@@ -119,30 +119,25 @@ public class SoundAreaState extends BaseAppState {
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) closePoly();
|
||||
if (placing) undoLastPoint();
|
||||
else deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
// get terrain hit
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
// snap to existing vertex?
|
||||
float[] snapped = snapVertex(hitX, hitZ);
|
||||
hitX = snapped[0];
|
||||
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) {
|
||||
float dx = hitX - currX.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();
|
||||
return;
|
||||
}
|
||||
@@ -152,21 +147,41 @@ public class SoundAreaState extends BaseAppState {
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} 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++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
if (pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||
selectArea(i);
|
||||
return;
|
||||
selectArea(i); 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();
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"sound".equals(input.zonenNewKind)) return;
|
||||
placing = true;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
currX.add(hitX);
|
||||
currZ.add(hitZ);
|
||||
// snap first point to nearby existing vertices
|
||||
float[] startSnap = snapVertex(hitX, hitZ);
|
||||
currX.add(startSnap[0]);
|
||||
currZ.add(startSnap[1]);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
}
|
||||
@@ -201,6 +216,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
PlacedSoundArea area = new PlacedSoundArea(xs, zs, "", 1f, false);
|
||||
addArea(area);
|
||||
selectArea(areas.size() - 1);
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
@@ -212,6 +228,14 @@ public class SoundAreaState extends BaseAppState {
|
||||
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 ────────────────────────────────────────────────────
|
||||
|
||||
private void updateInProgressGeo() {
|
||||
@@ -229,7 +253,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
float x = currX.get(currX.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;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
@@ -256,25 +280,41 @@ public class SoundAreaState extends BaseAppState {
|
||||
private void selectArea(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
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;
|
||||
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
areaGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(8f);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedSoundArea a = areas.get(idx);
|
||||
input.selectedSoundAreaInfo = idx + "|" + a.soundPath() + "|" + a.volume() + "|" + a.crossfade();
|
||||
input.soundAreaSelectionChanged = true;
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
input.zonenSelectedKind = "sound";
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||
@@ -325,7 +365,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
int n = xs.size();
|
||||
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
|
||||
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.flip();
|
||||
@@ -337,19 +377,44 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
public List<PlacedSoundArea> getPlacedAreas() {
|
||||
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) {
|
||||
if (rootNode == null) {
|
||||
pendingAreas = new ArrayList<>(loaded);
|
||||
@@ -359,7 +424,28 @@ public class SoundAreaState extends BaseAppState {
|
||||
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) {
|
||||
int n = xs.length;
|
||||
@@ -387,4 +473,66 @@ public class SoundAreaState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,9 +195,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void placePendingStoneAt(float wx, float wz) {
|
||||
if (terrain == null || pendingStone == null) { return; }
|
||||
float th = terrain.getHeight(new Vector2f(wx, wz));
|
||||
if (!Float.isFinite(th)) { return; }
|
||||
if (pendingStone == null) { return; }
|
||||
if (!Float.isFinite(getHeightAt(wx, wz))) { return; }
|
||||
PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(),
|
||||
pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed());
|
||||
int ci = chunkIndex(wx, wz);
|
||||
@@ -247,8 +246,7 @@ public class StoneEditorState extends BaseAppState {
|
||||
float sx = wx + (float)(Math.cos(angle) * dist);
|
||||
float sz = wz + (float)(Math.sin(angle) * dist);
|
||||
|
||||
float th = terrain.getHeight(new Vector2f(sx, sz));
|
||||
if (!Float.isFinite(th)) continue;
|
||||
if (!Float.isFinite(getHeightAt(sx, sz))) continue;
|
||||
|
||||
float radius = (float)(minR + random.nextDouble() * (maxR - minR));
|
||||
float rotY = random.nextFloat() * 360f;
|
||||
@@ -605,12 +603,71 @@ public class StoneEditorState extends BaseAppState {
|
||||
|
||||
private Vector3f raycastTerrain(float sx, float sy) {
|
||||
if (terrain == null) return null;
|
||||
Ray ray = new Ray(cam.getWorldCoordinates(new Vector2f(sx, sy), 0f),
|
||||
cam.getWorldCoordinates(new Vector2f(sx, sy), 1f));
|
||||
ray.getDirection().subtractLocal(ray.getOrigin()).normalizeLocal();
|
||||
Vector3f origin = cam.getWorldCoordinates(new Vector2f(sx, sy), 0f);
|
||||
Vector3f target = cam.getWorldCoordinates(new Vector2f(sx, sy), 1f);
|
||||
Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal());
|
||||
|
||||
CollisionResults res = new CollisionResults();
|
||||
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 ────────────────────────────────────────────────────────
|
||||
@@ -631,10 +688,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private float stoneWorldY(PlacedStone s) {
|
||||
if (terrain == null) return 0f;
|
||||
float h = terrain.getHeight(new Vector2f(s.x(), s.z()));
|
||||
if (!Float.isFinite(h)) return 0f;
|
||||
return h < -1e10f ? 0f : h;
|
||||
float h = getHeightAt(s.x(), s.z());
|
||||
return Float.isFinite(h) ? h : 0f;
|
||||
}
|
||||
|
||||
// ── Persistenz ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1062,6 +1062,12 @@ public class TerrainEditorState extends BaseAppState {
|
||||
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) {
|
||||
input.reloadPlacedOther = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
|
||||
@@ -1333,6 +1333,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
/** Raycast gegen reine Voxel-Geometrie; gibt nächsten Treffpunkt oder null zurück. */
|
||||
public Vector3f raycastVoxelGeometry(com.jme3.math.Ray ray) {
|
||||
if (voxelRoot == null) return null;
|
||||
CollisionResults results = new CollisionResults();
|
||||
voxelRoot.collideWith(ray, results);
|
||||
if (results.size() == 0) return null;
|
||||
|
||||
@@ -165,11 +165,8 @@ public class WaterBodyState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
@@ -273,15 +270,14 @@ public class WaterBodyState extends BaseAppState {
|
||||
private void updatePillarGeo() {
|
||||
if (pillarGeo != null) { rootNode.detachChild(pillarGeo); pillarGeo = null; }
|
||||
int n = currX.size();
|
||||
if (n == 0 || terrain == null) return;
|
||||
if (n == 0) return;
|
||||
|
||||
FloatBuffer pos = BufferUtils.createFloatBuffer(n * 2 * 3);
|
||||
FloatBuffer col = BufferUtils.createFloatBuffer(n * 2 * 4);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
float x = currX.get(i), z = currZ.get(i);
|
||||
Float th = terrain.getHeight(new com.jme3.math.Vector2f(x, z));
|
||||
float ty = th != null ? th : currentWaterHeight;
|
||||
float ty = getHeightAt(x, z);
|
||||
float wy = currentWaterHeight;
|
||||
ColorRGBA c = (ty < wy) ? PILLAR_SUB : PILLAR_DRY;
|
||||
|
||||
@@ -307,18 +303,15 @@ public class WaterBodyState extends BaseAppState {
|
||||
}
|
||||
|
||||
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 jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) { removeCursorPillar(); return; }
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) { removeCursorPillar(); return; }
|
||||
float ty = pt.y, wy = currentWaterHeight;
|
||||
float delta = wy - ty;
|
||||
ColorRGBA c = (delta > 0) ? PILLAR_SUB : PILLAR_DRY;
|
||||
@@ -432,16 +425,13 @@ public class WaterBodyState extends BaseAppState {
|
||||
// ── Height sampling ───────────────────────────────────────────────────────
|
||||
|
||||
private float sampleTerrainAtCursor() {
|
||||
if (terrain == null) return Float.NaN;
|
||||
float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return Float.NaN;
|
||||
return hits.getClosestCollision().getContactPoint().y;
|
||||
Vector3f pt = raycastAll(ray);
|
||||
return pt != null ? pt.y : Float.NaN;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
@@ -639,4 +629,66 @@ public class WaterBodyState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
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;
|
||||
@@ -35,6 +37,12 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
private TextField objectPathField;
|
||||
private VBox formContainer;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public CraftingTableEditorView(Path tableDir) {
|
||||
this.tableDir = tableDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -108,14 +116,11 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
|
||||
nameIdField = new TextField();
|
||||
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
|
||||
nameIdField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
objectPathField = new TextField();
|
||||
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
|
||||
|
||||
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());
|
||||
objectPathField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
form.getChildren().addAll(
|
||||
formTypeLabel,
|
||||
@@ -124,13 +129,22 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
row("Name-ID:", nameIdField),
|
||||
new Separator(),
|
||||
sectionTitle("3D-Objekt"),
|
||||
row("Pfad:", objectPathField),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
row("Pfad:", objectPathField)
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
private void persistCurrent() {
|
||||
if (currentType == null) { return; }
|
||||
saveCurrentTable();
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
|
||||
@@ -174,7 +188,7 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
try {
|
||||
CraftingTableIO.save(t, tableDir);
|
||||
} 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;
|
||||
}
|
||||
reloadMap();
|
||||
@@ -187,7 +201,7 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
try {
|
||||
CraftingTableIO.delete(currentType, tableDir);
|
||||
} 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;
|
||||
}
|
||||
reloadMap();
|
||||
|
||||
@@ -696,11 +696,11 @@ public class DialogEditorView extends BorderPane {
|
||||
}
|
||||
String id = result.get().trim();
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
DialogOption opt = new DialogOption();
|
||||
@@ -749,6 +749,7 @@ public class DialogEditorView extends BorderPane {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Option auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
ListView<String> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
@@ -794,6 +795,7 @@ public class DialogEditorView extends BorderPane {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Quest auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
ListView<Quest> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
@@ -1119,6 +1121,7 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
private static Trigger buildTriggerDialog(String type) {
|
||||
Dialog<Trigger> dlg = new Dialog<>();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Trigger konfigurieren: " + type);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
|
||||
21
blight-editor/src/main/java/de/blight/editor/ui/Dialogs.java
Normal file
21
blight-editor/src/main/java/de/blight/editor/ui/Dialogs.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -9,6 +10,7 @@ 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;
|
||||
@@ -41,6 +43,12 @@ public class FractionEditorView extends BorderPane {
|
||||
private TextField rank3Field;
|
||||
private VBox formContainer;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public FractionEditorView(Path fractionDir) {
|
||||
this.fractionDir = fractionDir;
|
||||
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;");
|
||||
|
||||
nameField = field("z. B. faction.guards");
|
||||
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
maleMemberField = field("z. B. faction.guards.member.male");
|
||||
maleMemberField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
femaleMemberField = field("z. B. faction.guards.member.female");
|
||||
femaleMemberField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
rank1Field = field("z. B. faction.guards.rank1");
|
||||
rank1Field.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
rank2Field = field("z. B. faction.guards.rank2");
|
||||
rank2Field.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
rank3Field = field("z. B. faction.guards.rank3");
|
||||
|
||||
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());
|
||||
rank3Field.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung"),
|
||||
@@ -144,17 +153,37 @@ public class FractionEditorView extends BorderPane {
|
||||
new Separator(),
|
||||
row("Rang 1:", rank1Field),
|
||||
row("Rang 2:", rank2Field),
|
||||
row("Rang 3:", rank3Field),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
row("Rang 3:", rank3Field)
|
||||
);
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
@@ -217,28 +246,6 @@ public class FractionEditorView extends BorderPane {
|
||||
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() {
|
||||
fractions.setAll(FractionIO.loadAll(fractionDir));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import de.blight.common.model.ItemIO;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.ObjectReference;
|
||||
import de.blight.common.model.TextReference;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -18,6 +19,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -51,6 +53,12 @@ public class ItemEditorView extends BorderPane {
|
||||
private VBox consumablesSection;
|
||||
private VBox effectsRows;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public ItemEditorView(Path itemDir) {
|
||||
this.itemDir = itemDir;
|
||||
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot
|
||||
@@ -188,6 +196,7 @@ public class ItemEditorView extends BorderPane {
|
||||
if (wasFocused && !isFocused) onIdCommitted();
|
||||
});
|
||||
idField.setOnAction(e -> onIdCommitted());
|
||||
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
catCombo = new ComboBox<>();
|
||||
catCombo.getItems().addAll(ItemCategory.values());
|
||||
@@ -237,15 +246,20 @@ public class ItemEditorView extends BorderPane {
|
||||
} else {
|
||||
subCatCombo.setValue(null);
|
||||
}
|
||||
scheduleSave();
|
||||
});
|
||||
subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
goldSpinner = new Spinner<>(0, 999999, 0);
|
||||
goldSpinner.setEditable(true);
|
||||
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
modelRefField = new TextField();
|
||||
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) -> {
|
||||
consumablesSection.setVisible(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(
|
||||
sectionTitle("Item"),
|
||||
new Separator(),
|
||||
@@ -310,9 +320,7 @@ public class ItemEditorView extends BorderPane {
|
||||
row("Modell:", modelFullRow),
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
consumablesSection
|
||||
);
|
||||
return form;
|
||||
}
|
||||
@@ -331,10 +339,32 @@ public class ItemEditorView extends BorderPane {
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
@@ -424,24 +454,6 @@ public class ItemEditorView extends BorderPane {
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void addEffectRow(CharacterStat stat, int value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import de.blight.common.model.TextBundle;
|
||||
import de.blight.common.model.TextBundleIO;
|
||||
import de.blight.common.model.TextKeyStore;
|
||||
import de.blight.common.model.TextRegistry;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -16,6 +17,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -39,6 +41,12 @@ public class LocalizationEditorView extends BorderPane {
|
||||
private final TableView<String[]> audioTable = new TableView<>(audioData);
|
||||
private AudioBundle currentAudioBundle = null;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> saveCurrent());
|
||||
}
|
||||
|
||||
public LocalizationEditorView(Path locDir) {
|
||||
this.locDir = locDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -58,13 +66,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
Button delLangBtn = new Button("- Sprache");
|
||||
delLangBtn.setOnAction(e -> deleteLanguage());
|
||||
|
||||
Button saveBtn = new Button("Speichern");
|
||||
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);
|
||||
HBox toolbar = new HBox(8, langLbl, langCombo, addLangBtn, delLangBtn);
|
||||
toolbar.setPadding(new Insets(8, 12, 8, 12));
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
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");
|
||||
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));
|
||||
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);
|
||||
|
||||
TableColumn<String[], String> valCol = new TableColumn<>("Text");
|
||||
valCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1]));
|
||||
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));
|
||||
VBox.setVgrow(table, Priority.ALWAYS);
|
||||
@@ -150,13 +152,13 @@ public class LocalizationEditorView extends BorderPane {
|
||||
TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel");
|
||||
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));
|
||||
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);
|
||||
|
||||
TableColumn<String[], String> pathCol = new TableColumn<>("Datei");
|
||||
pathCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1]));
|
||||
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<>("");
|
||||
browseCol.setPrefWidth(36);
|
||||
@@ -179,6 +181,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
if (file != null) {
|
||||
audioData.get(idx)[1] = file.getAbsolutePath();
|
||||
audioTable.refresh();
|
||||
scheduleSave();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -234,6 +237,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private void addLanguage() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Sprache hinzufuegen");
|
||||
dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):");
|
||||
dlg.showAndWait().ifPresent(lang -> {
|
||||
@@ -245,7 +249,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
refreshLangList();
|
||||
langCombo.setValue(lang);
|
||||
} 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() {
|
||||
String lang = langCombo.getValue();
|
||||
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);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
if (bt == ButtonType.YES) {
|
||||
@@ -266,7 +270,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
audioData.clear();
|
||||
refreshLangList();
|
||||
} 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()) {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Schluessel hinzufuegen");
|
||||
dlg.setHeaderText("Alle bekannten Schluessel bereits vorhanden.\nNeuen Schluessel eingeben:");
|
||||
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<>();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Schluessel waehlen");
|
||||
dlg.setHeaderText("Noch nicht uebersetzte Schluessel:");
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
@@ -315,36 +321,45 @@ public class LocalizationEditorView extends BorderPane {
|
||||
tableData.add(new String[]{key, ""});
|
||||
table.scrollTo(tableData.size() - 1);
|
||||
table.getSelectionModel().select(tableData.size() - 1);
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
private void deleteKey() {
|
||||
String[] sel = table.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) tableData.remove(sel);
|
||||
if (sel != null) { tableData.remove(sel); scheduleSave(); }
|
||||
}
|
||||
|
||||
// Audio-Eintrags-Verwaltung
|
||||
|
||||
private void addAudioKey() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Audio-Schluessel hinzufuegen");
|
||||
dlg.setHeaderText("Schluessel eingeben (muss dem AudioReference-Key im Dialog entsprechen):");
|
||||
dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(key -> {
|
||||
audioData.add(new String[]{key, ""});
|
||||
audioTable.scrollTo(audioData.size() - 1);
|
||||
audioTable.getSelectionModel().select(audioData.size() - 1);
|
||||
scheduleSave();
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteAudioKey() {
|
||||
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
|
||||
|
||||
private void saveCurrent() {
|
||||
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;
|
||||
}
|
||||
Map<String, String> textEntries = new LinkedHashMap<>();
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.blight.editor.ui;
|
||||
import de.blight.common.LocationIO;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -10,33 +11,30 @@ 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.util.List;
|
||||
|
||||
/**
|
||||
* Locations-Verwaltung: Liste links, Formular rechts.
|
||||
* Alle Locations werden gemeinsam in einer Datei gespeichert (LocationIO).
|
||||
*/
|
||||
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 Button deleteBtn;
|
||||
private Location current = null;
|
||||
private boolean reloading = false;
|
||||
|
||||
// ── Form fields ───────────────────────────────────────────────────────────
|
||||
|
||||
private TextField nameIdField;
|
||||
private TextField centerXField;
|
||||
private TextField centerZField;
|
||||
private TextField radiusField;
|
||||
private TextField idField;
|
||||
private Label nameKeyLabel;
|
||||
private TriggerListEditor triggerEditor;
|
||||
private VBox formContainer;
|
||||
|
||||
{ savePause.setOnFinished(e -> persistCurrent()); }
|
||||
|
||||
public LocationEditorView() {
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
@@ -46,6 +44,18 @@ public class LocationEditorView extends BorderPane {
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
@@ -55,7 +65,9 @@ public class LocationEditorView extends BorderPane {
|
||||
@Override protected void updateItem(Location loc, boolean empty) {
|
||||
super.updateItem(loc, empty);
|
||||
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;"
|
||||
+ " -fx-border-color: transparent transparent transparent #66aacc;"
|
||||
+ " -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.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
nameIdField = field("z. B. location.village");
|
||||
centerXField = field("X-Koordinate");
|
||||
centerZField = field("Z-Koordinate");
|
||||
radiusField = field("Radius in Meter");
|
||||
idField = field("z. B. village");
|
||||
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(); }
|
||||
});
|
||||
|
||||
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(
|
||||
sectionTitle("Kennung & Position"),
|
||||
sectionTitle("Kennung"),
|
||||
new Separator(),
|
||||
row("Name-ID:", nameIdField),
|
||||
row("Mitte X:", centerXField),
|
||||
row("Mitte Z:", centerZField),
|
||||
row("Radius:", radiusField),
|
||||
row("ID:", idField),
|
||||
nameKeyLabel,
|
||||
sectionTitle("Trigger"),
|
||||
new Separator(),
|
||||
triggerEditor,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
triggerEditor
|
||||
);
|
||||
return form;
|
||||
}
|
||||
@@ -139,7 +149,8 @@ public class LocationEditorView extends BorderPane {
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onSelected(Location old, Location nw) {
|
||||
if (old != null) saveFormToLocation(old);
|
||||
if (reloading) return;
|
||||
if (old != null) { saveFormToLocation(old); persist(); }
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
||||
@@ -147,38 +158,34 @@ public class LocationEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void loadForm(Location loc) {
|
||||
nameIdField.setText(loc.getId());
|
||||
centerXField.setText(String.valueOf(loc.getCenterX()));
|
||||
centerZField.setText(String.valueOf(loc.getCenterZ()));
|
||||
radiusField.setText(String.valueOf(loc.getRadius()));
|
||||
String fullId = loc.getId();
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
idField.setText(shortId);
|
||||
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
|
||||
|
||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||
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);
|
||||
}
|
||||
|
||||
private void saveFormToLocation(Location loc) {
|
||||
String nameId = nameIdField.getText().trim();
|
||||
loc.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||
loc.setCenterX(parseFloat(centerXField.getText()));
|
||||
loc.setCenterZ(parseFloat(centerZField.getText()));
|
||||
loc.setRadius(parseFloat(radiusField.getText()));
|
||||
String shortId = idField.getText().trim();
|
||||
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
|
||||
loc.setName(fullId.isBlank() ? null : new TextReference(fullId));
|
||||
loc.setTriggers(triggerEditor.getTriggers());
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
nameIdField.clear();
|
||||
centerXField.clear();
|
||||
centerZField.clear();
|
||||
radiusField.clear();
|
||||
idField.clear();
|
||||
nameKeyLabel.setText("Name-Key: —");
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createLocation() {
|
||||
Location loc = new Location();
|
||||
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis()));
|
||||
loc.setName(new TextReference(PREFIX + "neu_" + System.currentTimeMillis()));
|
||||
locations.add(loc);
|
||||
listView.getSelectionModel().select(loc);
|
||||
}
|
||||
@@ -194,26 +201,10 @@ public class LocationEditorView extends BorderPane {
|
||||
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() {
|
||||
try { LocationIO.save(locations); }
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static float parseFloat(String s) {
|
||||
try { return Float.parseFloat(s.trim().replace(',', '.')); }
|
||||
catch (NumberFormatException ignored) { return 0f; }
|
||||
}
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
Label l = new Label(text);
|
||||
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) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(80);
|
||||
lbl.setMinWidth(50);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
|
||||
@@ -115,11 +115,9 @@ public class MapObjectsView extends VBox {
|
||||
// ── Löschen ───────────────────────────────────────────────────────────────
|
||||
|
||||
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.setHeaderText("Objekt wirklich löschen?");
|
||||
alert.setContentText(label);
|
||||
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
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());
|
||||
refresh();
|
||||
} catch (Exception ex) {
|
||||
Alert err = new Alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK);
|
||||
err.showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,12 +240,12 @@ public class MapObjectsView extends VBox {
|
||||
private void loadAreas() {
|
||||
try {
|
||||
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++) {
|
||||
PlacedArea a = list.get(idx);
|
||||
float cx = centroid(a.pointsX()), cz = centroid(a.pointsZ());
|
||||
String label = a.nameId() != null && !a.nameId().isBlank()
|
||||
? a.nameId() : "Bereich #" + (idx + 1);
|
||||
String label = a.areaId() != null && !a.areaId().isBlank()
|
||||
? a.areaId() : "Area #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "area", a, idx));
|
||||
group.getChildren().add(item);
|
||||
|
||||
@@ -34,6 +34,7 @@ public class MaterialChooser extends Dialog<String> {
|
||||
public MaterialChooser(Path matDefsRoot) {
|
||||
setTitle("Material auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
VBox contentBox = new VBox(12);
|
||||
|
||||
@@ -39,6 +39,7 @@ public class ModelChooser extends Dialog<String> {
|
||||
this.assetRoot = assetRoot;
|
||||
setTitle("Modell auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
contentBox.setPadding(new Insets(4));
|
||||
|
||||
@@ -237,7 +237,7 @@ public class MonologueEditorView extends SplitPane {
|
||||
try {
|
||||
MonologueIO.save(new ArrayList<>(monologues));
|
||||
} 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() {
|
||||
Monologue sel = listView.getSelectionModel().getSelectedItem();
|
||||
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);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
if (bt == ButtonType.YES) {
|
||||
@@ -296,6 +296,7 @@ public class MonologueEditorView extends SplitPane {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Quest auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
ListView<Quest> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import de.blight.common.model.NPC;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import de.blight.common.model.quests.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -14,6 +15,7 @@ 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;
|
||||
@@ -43,13 +45,19 @@ public class QuestEditorView extends BorderPane {
|
||||
private ComboBox<String> typeCombo;
|
||||
private VBox dynamicArea;
|
||||
private VBox formContainer;
|
||||
private Button saveBtn;
|
||||
|
||||
// ── Type-specific fields ──────────────────────────────────────────────────
|
||||
|
||||
private TextField f1, f2, f3;
|
||||
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) {
|
||||
this.questDir = questDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -134,17 +142,22 @@ public class QuestEditorView extends BorderPane {
|
||||
idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
|
||||
if (!isFocused) autoFillTextRefs();
|
||||
});
|
||||
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
xpSpinner = new Spinner<>(0, 99999, 0);
|
||||
xpSpinner.setEditable(true);
|
||||
xpSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
xpSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
textField = new TextField();
|
||||
textField.setPromptText("TextReference-Schlüssel");
|
||||
textField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
successField = new TextField();
|
||||
successField.setPromptText("TextReference-Schlüssel");
|
||||
successField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Quest"),
|
||||
@@ -163,7 +176,7 @@ public class QuestEditorView extends BorderPane {
|
||||
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
|
||||
typeCombo.setPromptText("Typ auswählen…");
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue()));
|
||||
typeCombo.setOnAction(e -> { rebuildDynamicArea(typeCombo.getValue()); scheduleSave(); });
|
||||
|
||||
dynamicArea = new VBox(6);
|
||||
|
||||
@@ -174,12 +187,6 @@ public class QuestEditorView extends BorderPane {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
@@ -406,30 +443,6 @@ public class QuestEditorView extends BorderPane {
|
||||
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() {
|
||||
List<Quest> loaded = QuestIO.loadAll(questDir);
|
||||
quests.setAll(loaded);
|
||||
@@ -467,7 +480,6 @@ public class QuestEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void showError(String msg) {
|
||||
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
||||
a.showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, msg, ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -10,6 +11,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -49,6 +51,12 @@ public class RecipeEditorView extends BorderPane {
|
||||
private Spinner<Integer> engineeringSpinner;
|
||||
private VBox formContainer;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public RecipeEditorView(Path recipeDir) {
|
||||
this.recipeDir = recipeDir;
|
||||
this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
|
||||
@@ -129,6 +137,7 @@ public class RecipeEditorView extends BorderPane {
|
||||
|
||||
createsField = new TextField();
|
||||
createsField.setPromptText("Item-ID des erstellten Items");
|
||||
createsField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
componentsList = new ListView<>();
|
||||
componentsList.setPrefHeight(110);
|
||||
@@ -156,7 +165,7 @@ public class RecipeEditorView extends BorderPane {
|
||||
}
|
||||
tableCombo.setValue(NO_TABLE);
|
||||
tableCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue()));
|
||||
tableCombo.setOnAction(e -> { updateRequirementRows(tableCombo.getValue()); scheduleSave(); });
|
||||
|
||||
alchemySpinner = lvlSpinner();
|
||||
enchantingSpinner = lvlSpinner();
|
||||
@@ -168,11 +177,6 @@ public class RecipeEditorView extends BorderPane {
|
||||
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
|
||||
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(
|
||||
sectionTitle("Ergebnis"),
|
||||
new Separator(),
|
||||
@@ -186,9 +190,7 @@ public class RecipeEditorView extends BorderPane {
|
||||
alchemyRow,
|
||||
enchantingRow,
|
||||
smitheryRow,
|
||||
engineeringRow,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
engineeringRow
|
||||
);
|
||||
|
||||
updateRequirementRows(NO_TABLE);
|
||||
@@ -216,10 +218,38 @@ public class RecipeEditorView extends BorderPane {
|
||||
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 ──────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
@@ -342,38 +372,11 @@ public class RecipeEditorView extends BorderPane {
|
||||
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() {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Zutat hinzufügen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
TextField itemIdField = new TextField();
|
||||
itemIdField.setPromptText("Item-ID");
|
||||
|
||||
@@ -462,6 +462,7 @@ public class RoutineEditorView extends VBox {
|
||||
|
||||
private void addRoutine() {
|
||||
TextInputDialog dlg = new TextInputDialog("Routine " + (routines.size() + 1));
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setHeaderText("Name des Tagesablaufs:");
|
||||
dlg.showAndWait().ifPresent(name -> {
|
||||
if (name.isBlank()) return;
|
||||
@@ -475,6 +476,7 @@ public class RoutineEditorView extends VBox {
|
||||
private void renameRoutine() {
|
||||
if (activeRoutine == null) return;
|
||||
TextInputDialog dlg = new TextInputDialog(activeRoutine.getName());
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setHeaderText("Neuer Name:");
|
||||
dlg.showAndWait().ifPresent(name -> {
|
||||
if (!name.isBlank()) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ public class TextureChooser extends Dialog<String> {
|
||||
public TextureChooser(Path assetRoot, boolean includeJmeBuiltin) {
|
||||
setTitle("Textur auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
contentBox.setPadding(new Insets(4));
|
||||
|
||||
@@ -5,6 +5,8 @@ import de.blight.common.model.Monologue;
|
||||
import de.blight.common.model.QuestRef;
|
||||
import de.blight.common.model.Status;
|
||||
import de.blight.common.model.trigger.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -12,19 +14,9 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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> {
|
||||
|
||||
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_ROUTINE = "Routine ändern";
|
||||
private static final String TYPE_MONOLOGUE = "Monolog starten";
|
||||
private static final String TYPE_NPC_DIALOG = "NPC-Dialog starten";
|
||||
|
||||
// Gemeinsam
|
||||
private final ComboBox<String> typeCombo = new ComboBox<>();
|
||||
@@ -56,33 +49,89 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
// Monolog
|
||||
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() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/** Öffnet den Dialog im Bearbeitungsmodus mit einem vorhandenen Trigger. */
|
||||
public TriggerDialog(Trigger existing) {
|
||||
setTitle(existing == null ? "Trigger hinzufügen" : "Trigger bearbeiten");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
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.setOnAction(e -> rebuildDynamic(typeCombo.getValue()));
|
||||
|
||||
chapterSpinner.setEditable(true);
|
||||
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);
|
||||
content.setPadding(new Insets(16));
|
||||
content.setPrefWidth(400);
|
||||
|
||||
content.setPrefWidth(420);
|
||||
content.getChildren().addAll(
|
||||
row("Trigger-Typ:", typeCombo),
|
||||
row("Kapitel (mind.):", chapterSpinner),
|
||||
new Separator(),
|
||||
dynamicArea
|
||||
dynamicArea,
|
||||
new Separator(),
|
||||
condSectionLbl,
|
||||
row("Verknüpfung:", conditionModeCombo),
|
||||
conditionList,
|
||||
condButtons
|
||||
);
|
||||
|
||||
getDialogPane().setContent(content);
|
||||
@@ -90,14 +139,10 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
|
||||
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
|
||||
// Validierung: OK nur wenn Typ gewählt und Pflichtfelder gefüllt
|
||||
typeCombo.valueProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null));
|
||||
|
||||
// Ergebnis-Konverter
|
||||
setResultConverter(bt -> bt == ButtonType.OK ? buildTrigger() : null);
|
||||
|
||||
// Vorhandenen Trigger laden
|
||||
if (existing != null) preload(existing);
|
||||
else typeCombo.setValue(TYPE_QUEST);
|
||||
}
|
||||
@@ -113,15 +158,13 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
case TYPE_FRACTION -> buildFractionFields();
|
||||
case TYPE_ROUTINE -> buildRoutineFields();
|
||||
case TYPE_MONOLOGUE -> buildMonologueFields();
|
||||
case TYPE_NPC_DIALOG -> buildNpcDialogFields();
|
||||
}
|
||||
}
|
||||
|
||||
private void buildQuestFields() {
|
||||
questIdField = field("Quest-ID (z. B. q_main_001)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("Quest"),
|
||||
row("Quest-ID:", questIdField)
|
||||
);
|
||||
dynamicArea.getChildren().addAll(sectionTitle("Quest"), row("Quest-ID:", questIdField));
|
||||
}
|
||||
|
||||
private void buildNpcFields() {
|
||||
@@ -160,14 +203,19 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
monologueIdCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
monologueIdCombo.setPromptText("Monolog-ID eingeben oder wählen...");
|
||||
try {
|
||||
de.blight.common.MonologueIO.load().stream()
|
||||
MonologueIO.load().stream()
|
||||
.map(Monologue::getId)
|
||||
.filter(id -> !id.isBlank())
|
||||
.forEach(monologueIdCombo.getItems()::add);
|
||||
} 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(
|
||||
sectionTitle("Monolog starten"),
|
||||
row("Monolog-ID:", monologueIdCombo)
|
||||
sectionTitle("NPC-Dialog starten"),
|
||||
row("NPC-ID:", npcDialogIdField)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,16 +263,29 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
}
|
||||
yield mo;
|
||||
}
|
||||
case TYPE_NPC_DIALOG -> {
|
||||
NpcDialogTrigger nd = new NpcDialogTrigger();
|
||||
if (npcDialogIdField != null) nd.setNpcId(npcDialogIdField.getText().trim());
|
||||
yield nd;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// ── Vorhandenen Trigger vorfüllen ─────────────────────────────────────────
|
||||
// ── Vorladen ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void preload(Trigger t) {
|
||||
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) {
|
||||
typeCombo.setValue(TYPE_QUEST);
|
||||
if (questIdField != null && q.getQuest() != null)
|
||||
@@ -242,14 +303,16 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
fractionStatusCombo.setValue(f.getTargetStatus());
|
||||
} else if (t instanceof ChangeRoutineTrigger r) {
|
||||
typeCombo.setValue(TYPE_ROUTINE);
|
||||
if (routineNpcIdField != null && r.getNpcId() != null)
|
||||
routineNpcIdField.setText(r.getNpcId());
|
||||
if (routineNameField != null && r.getRoutineName() != null)
|
||||
routineNameField.setText(r.getRoutineName());
|
||||
if (routineNpcIdField != null && r.getNpcId() != null) routineNpcIdField.setText(r.getNpcId());
|
||||
if (routineNameField != null && r.getRoutineName() != null) routineNameField.setText(r.getRoutineName());
|
||||
} else if (t instanceof MonologueTrigger mo) {
|
||||
typeCombo.setValue(TYPE_MONOLOGUE);
|
||||
if (monologueIdCombo != null && mo.getMonologueId() != null)
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ public class TriggerListEditor extends VBox {
|
||||
+ " -> \"" + nullSafe(r.getRoutineName()) + "\"" + chapter;
|
||||
if (t instanceof MonologueTrigger mo)
|
||||
return "Monolog: " + nullSafe(mo.getMonologueId()) + chapter;
|
||||
if (t instanceof NpcDialogTrigger nd)
|
||||
return "NPC-Dialog: " + nullSafe(nd.getNpcId()) + chapter;
|
||||
return t.getClass().getSimpleName() + chapter;
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,8 @@ public class JmeConsole extends BaseAppState {
|
||||
|
||||
private BitmapText makeLine(BitmapFont font, ColorRGBA color, float x, float y) {
|
||||
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.setLocalTranslation(x, y, 0f);
|
||||
return t;
|
||||
|
||||
@@ -102,6 +102,7 @@ public class WorldScene extends BaseAppState {
|
||||
private float spawnYaw = 0f;
|
||||
private de.blight.game.state.OceanSoundState oceanSound;
|
||||
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.state.DrownState drownState;
|
||||
private WaterInteractionState waterInteractionState;
|
||||
@@ -430,8 +431,12 @@ public class WorldScene extends BaseAppState {
|
||||
app.getListener().setRotation(app.getCamera().getRotation());
|
||||
if (oceanSound != null) oceanSound.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) {
|
||||
terrainMaterial.setVector3("LightDir", dayNight.getSunDirection().negate());
|
||||
ColorRGBA sc = dayNight.getSunLight().getColor();
|
||||
@@ -945,6 +950,9 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
ambientSounds = new de.blight.game.state.AmbientSoundSystem();
|
||||
app.getStateManager().attach(ambientSounds);
|
||||
|
||||
musicSystem = new de.blight.game.state.MusicSystem();
|
||||
app.getStateManager().attach(musicSystem);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -285,6 +285,14 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
}
|
||||
cc.a = 1f;
|
||||
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
|
||||
|
||||
@@ -867,7 +867,8 @@ public class DialogHudState extends BaseAppState {
|
||||
|
||||
private BitmapText makeTxt(String s, float size, ColorRGBA color) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size);
|
||||
t.setSize(size * 2f);
|
||||
t.setLocalScale(0.5f, 0.5f, 1f);
|
||||
t.setColor(color);
|
||||
t.setText(s);
|
||||
return t;
|
||||
|
||||
@@ -264,7 +264,8 @@ public class HotbarState extends BaseAppState {
|
||||
|
||||
private BitmapText txt(String s, int size, ColorRGBA col) {
|
||||
BitmapText t = new BitmapText(font);
|
||||
t.setSize(size);
|
||||
t.setSize(size * 2f);
|
||||
t.setLocalScale(0.5f, 0.5f, 1f);
|
||||
t.setColor(col);
|
||||
t.setText(s);
|
||||
t.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
|
||||
@@ -43,7 +43,8 @@ public class InteractionHudState extends BaseAppState {
|
||||
|
||||
BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||
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.setCullHint(Spatial.CullHint.Always);
|
||||
guiNode.attachChild(labelText);
|
||||
|
||||
@@ -6,63 +6,107 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.audio.AudioData;
|
||||
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.scene.Node;
|
||||
import de.blight.common.AreaDefinition;
|
||||
import de.blight.common.AreaDefinitionIO;
|
||||
import de.blight.common.AreaIO;
|
||||
import de.blight.common.PlacedArea;
|
||||
import de.blight.lang.TextResolver;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Proximity-based ambient music per polygon area.
|
||||
* Three parallel tracks (day / night / combat) play when the player is inside.
|
||||
* All three are attached + played on entry and detached on exit.
|
||||
* Which one is actually audible is controlled via volume (wiring to day/night deferred).
|
||||
* Area-based music system.
|
||||
*
|
||||
* Track priority: combat (not yet implemented) > night > day (fallback).
|
||||
* 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 {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MusicSystem.class);
|
||||
private static final float CHECK_INTERVAL = 0.25f;
|
||||
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 enum Phase { INACTIVE, FADING_IN, ACTIVE, FINISHING, FADING_OUT }
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private AudioSettingsState audioSettings;
|
||||
|
||||
private final List<PlacedArea> data = new ArrayList<>();
|
||||
// three nodes per area: [0]=day, [1]=night, [2]=combat; element may be null
|
||||
private final List<AudioNode[]> tracks = new ArrayList<>();
|
||||
private final List<FadeState> fadeStates = 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 float checkTimer = 0f;
|
||||
private boolean isDaytime = true;
|
||||
|
||||
private BitmapText areaNameText;
|
||||
private float areaNameTimer = 0f;
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
assets = app.getAssetManager();
|
||||
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 {
|
||||
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()) {
|
||||
AudioNode[] arr = {
|
||||
loadAmbient(area.dayTrack()),
|
||||
loadAmbient(area.nightTrack()),
|
||||
loadAmbient(area.combatTrack())
|
||||
};
|
||||
boolean hasAny = false;
|
||||
for (AudioNode n : arr) if (n != null) { hasAny = true; break; }
|
||||
if (!hasAny) continue;
|
||||
AreaDefinition def = defs.get(area.areaId());
|
||||
if (def == null) continue;
|
||||
AudioNode day = loadTrack(def.dayTrack());
|
||||
AudioNode night = loadTrack(def.nightTrack());
|
||||
if (day == null && night == null) continue;
|
||||
String rawId = def.id();
|
||||
String shortId = rawId.startsWith("area.") ? rawId.substring("area.".length()) : rawId;
|
||||
data.add(area);
|
||||
tracks.add(arr);
|
||||
fadeStates.add(FadeState.INACTIVE);
|
||||
areaNameKeys.add(def.nameKey());
|
||||
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());
|
||||
} catch (IOException e) {
|
||||
@@ -72,60 +116,73 @@ public class MusicSystem extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
if (fadeStates.get(i) != FadeState.INACTIVE) {
|
||||
for (AudioNode n : tracks.get(i)) {
|
||||
if (n != null) { n.stop(); rootNode.detachChild(n); }
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
if (phases.get(i) != Phase.INACTIVE) stopAll(i);
|
||||
}
|
||||
data.clear();
|
||||
tracks.clear();
|
||||
fadeStates.clear();
|
||||
areaNameKeys.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 onDisable() {}
|
||||
|
||||
public void setPlayerPosition(Vector3f pos) {
|
||||
playerPos.set(pos);
|
||||
public void setPlayerPosition(Vector3f 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
|
||||
public void update(float tpf) {
|
||||
// area name display timer
|
||||
if (areaNameTimer > 0f) {
|
||||
areaNameTimer -= tpf;
|
||||
if (areaNameTimer <= 0f) {
|
||||
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 0f));
|
||||
areaNameTimer = 0f;
|
||||
} else if (areaNameTimer < AREA_NAME_FADE) {
|
||||
float alpha = areaNameTimer / AREA_NAME_FADE;
|
||||
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, alpha));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.isEmpty()) return;
|
||||
|
||||
for (int i = 0; i < tracks.size(); i++) {
|
||||
FadeState fs = fadeStates.get(i);
|
||||
if (fs == FadeState.INACTIVE || fs == FadeState.ACTIVE) continue;
|
||||
|
||||
AudioNode[] arr = tracks.get(i);
|
||||
if (fs == FadeState.FADING_IN) {
|
||||
boolean done = true;
|
||||
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);
|
||||
// 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;
|
||||
if (checkTimer < CHECK_INTERVAL) return;
|
||||
checkTimer = 0f;
|
||||
@@ -133,27 +190,138 @@ public class MusicSystem extends BaseAppState {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
PlacedArea area = data.get(i);
|
||||
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 (fs == FadeState.INACTIVE) {
|
||||
log.info("[MusicSystem] Bereich {} betreten → starte Tracks", i);
|
||||
for (AudioNode n : tracks.get(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);
|
||||
if (inside && (ph == Phase.INACTIVE || ph == Phase.FADING_OUT)) {
|
||||
startArea(i);
|
||||
} else if (!inside && (ph == Phase.ACTIVE || ph == Phase.FADING_IN || ph == Phase.FINISHING)) {
|
||||
beginFadeOut(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
// Use Stream for music (files are large), Buffer for short sfx
|
||||
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream);
|
||||
n.setLooping(true);
|
||||
n.setPositional(false);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,7 @@ dialog.silas.wer_seid_ihr.textnpc
|
||||
erzmoss.description
|
||||
erzmoss.name
|
||||
hero.name
|
||||
location.neu_1786533928723
|
||||
location.neu_1786546178407
|
||||
location.test
|
||||
silas.name
|
||||
|
||||
2
blight-map/src/main/map/blight_area_definitions.bad
Normal file
2
blight-map/src/main/map/blight_area_definitions.bad
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# 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 []
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user