Weitere Anpassungen bezüglich der Voxel und des Zoning Systems

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

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
package de.blight.common;
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;

View File

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

View File

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

View File

@@ -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>();
@@ -52,6 +67,11 @@ public class MainCharacter extends GameCharacter {
@Getter(AccessLevel.NONE)
@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) {
@@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,17 +4,38 @@ import de.blight.common.model.MainCharacter;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
public abstract class Trigger {
private int requiresChapter;
public boolean isTriggarable(MainCharacter character) {
return character.getChapter() >= requiresChapter && isTriggarableDelegate(character);
}
public abstract boolean isTriggarableDelegate(MainCharacter character);
private String triggerId = UUID.randomUUID().toString();
private int requiresChapter;
private ConditionMode conditionMode = ConditionMode.ALL;
private List<Condition> conditions = new ArrayList<>();
public abstract void trigger(MainCharacter character);
public boolean isTriggarable(MainCharacter 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);
public abstract void trigger(MainCharacter character);
}

View File

@@ -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)
@@ -86,10 +77,12 @@ public final class TriggerIO {
if (f.getTargetStatus() != null)
obj.addProperty("targetStatus", f.getTargetStatus().name());
} else if (src instanceof ChangeRoutineTrigger r) {
if (r.getNpcId() != null) obj.addProperty("npcId", r.getNpcId());
if (r.getNpcId() != null) obj.addProperty("npcId", r.getNpcId());
if (r.getRoutineName() != null) obj.addProperty("routineName", r.getRoutineName());
} 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";
}