Begonnen: Verhalten von Gegnern bei annäherung, Verfolgung etc

This commit is contained in:
2026-09-27 00:12:26 +02:00
parent 94e0cac10d
commit da8e5e99f1
34 changed files with 2220 additions and 887 deletions

View File

@@ -1276,6 +1276,17 @@ public class AnimPreviewState extends BaseAppState {
assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
}
// Eingebettete Texturen VOR dem Wrench-Load extrahieren: Wrench ersetzt
// Texturen mit nicht auffindbaren Pfaden sofort durch Platzhalter; danach
// ist der Original-Dateiname nicht mehr aus dem Modell ablesbar.
java.io.File preExtractDir = extractEmbeddedTexturesPreLoad(file);
if (preExtractDir != null) {
assets.registerLocator(preExtractDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
LOG.info("[CharImport] {} Textur(en) vorab extrahiert nach '{}'.",
preExtractDir.list() != null ? preExtractDir.list().length : 0,
preExtractDir.getAbsolutePath());
}
com.github.stephengold.wrench.LwjglAssetKey key =
new com.github.stephengold.wrench.LwjglAssetKey(file.getName());
key.setVerboseLogging(true);
@@ -1289,6 +1300,8 @@ public class AnimPreviewState extends BaseAppState {
return;
}
// Post-Load-Fallback: sollte durch Pre-Load nicht mehr nötig sein,
// aber schadet nicht als Sicherheitsnetz.
java.io.File embeddedDir = extractEmbeddedTextures(file, model);
if (embeddedDir != null) {
assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
@@ -1578,8 +1591,11 @@ public class AnimPreviewState extends BaseAppState {
}
/**
* Sucht eingebettete PNG-Texturen im FBX-Binär (Dateiname → PNG-Magic → IEND)
* Sucht eingebettete PNG- und JPEG-Texturen im FBX-Binär (Dateiname → Bild-Magic → Daten)
* und schreibt sie in ein temporäres Verzeichnis.
*
* PNG: Dateiname → PNG-Magic → IEND-Marker
* JPEG: Dateiname → (optional: FBX R-Property-Header mit Länge) → JPEG-Magic → Daten
*/
private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) {
java.util.Set<String> filenames = collectTextureFilenames(model);
@@ -1589,20 +1605,51 @@ public class AnimPreviewState extends BaseAppState {
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
"fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
tempDir.mkdirs();
byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82};
byte[] pngMagic = {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
byte[] iend = {0x00,0x00,0x00,0x00, 0x49,0x45,0x4E,0x44, (byte)0xAE,0x42,0x60,(byte)0x82};
byte[] jpegMagic = {(byte)0xFF, (byte)0xD8, (byte)0xFF};
byte[] jpegEoi = {(byte)0xFF, (byte)0xD9};
boolean any = false;
for (String filename : filenames) {
byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
int namePos = binaryIndexOf(data, nameBytes, 0);
if (namePos < 0) continue;
// ── PNG ───────────────────────────────────────────────────────
int pngPos = binaryIndexOf(data, pngMagic, namePos);
if (pngPos < 0 || pngPos - namePos > 65536) continue;
int endPos = binaryIndexOf(data, iend, pngPos + 8);
if (endPos < 0) continue;
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length);
if (pngPos >= 0 && pngPos - namePos <= 65536) {
int endPos = binaryIndexOf(data, iend, pngPos + 8);
if (endPos >= 0) {
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
LOG.info("[FbxImport] Embedded PNG extrahiert: {} ({} B)", filename, png.length);
any = true;
continue;
}
}
// ── JPEG ──────────────────────────────────────────────────────
int jpegPos = binaryIndexOf(data, jpegMagic, namePos);
if (jpegPos < 0 || jpegPos - namePos > 65536) continue;
// FBX R-Property: 0x52 + 4-Byte LE-Größe direkt vor den JPEG-Daten
int jpegSize = 0;
if (jpegPos >= 5 && data[jpegPos - 5] == (byte) 0x52) {
jpegSize = java.nio.ByteBuffer.wrap(data, jpegPos - 4, 4)
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt();
}
byte[] jpeg;
if (jpegSize > 1024 && jpegPos + jpegSize <= data.length) {
// Exakte Größe aus FBX-Header bekannt
jpeg = java.util.Arrays.copyOfRange(data, jpegPos, jpegPos + jpegSize);
} else {
// Fallback: bis EOI-Marker scannen
int eoiPos = binaryIndexOf(data, jpegEoi, jpegPos + 2);
if (eoiPos < 0) continue;
jpeg = java.util.Arrays.copyOfRange(data, jpegPos, eoiPos + 2);
}
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), jpeg);
LOG.info("[FbxImport] Embedded JPEG extrahiert: {} ({} B)", filename, jpeg.length);
any = true;
}
return any ? tempDir : null;
@@ -1612,6 +1659,122 @@ public class AnimPreviewState extends BaseAppState {
}
}
/**
* Liest das FBX-Binary, findet alle eingebetteten Bild-Texturen anhand von
* FBX-R-Property-Headern und Dateinamen-Strings, und schreibt sie (URL-dekodiert)
* in ein temporäres Verzeichnis — BEVOR das Modell mit dem Wrench-Loader geladen wird.
*/
private java.io.File extractEmbeddedTexturesPreLoad(java.io.File fbxFile) {
try {
byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath());
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
"fbxtex_pre_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
tempDir.mkdirs();
boolean any = false;
byte[] pngMagic = {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
byte[] iend = {0x00,0x00,0x00,0x00, 0x49,0x45,0x4E,0x44, (byte)0xAE,0x42,0x60,(byte)0x82};
byte[] jpegMagic = {(byte)0xFF, (byte)0xD8, (byte)0xFF};
// ── JPEG: R-Property Header (0x52 + 4-Byte LE Größe + JPEG Magic) ──
int searchPos = 0;
while (true) {
int jpegPos = binaryIndexOf(data, jpegMagic, searchPos);
if (jpegPos < 0) break;
searchPos = jpegPos + 1;
if (jpegPos < 5 || data[jpegPos - 5] != (byte) 0x52) continue;
int size = java.nio.ByteBuffer.wrap(data, jpegPos - 4, 4)
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt();
if (size < 1024 || jpegPos + size > data.length) continue;
// Dateiname: letztes .jpg/.jpeg-Vorkommen vor jpegPos (max 65536 Bytes rückwärts)
String decodedName = findImageFilenameBeforePos(data, jpegPos, new String[]{".jpg", ".jpeg"});
if (decodedName == null) decodedName = "texture_" + jpegPos + ".jpg";
java.io.File dest = new java.io.File(tempDir, decodedName);
if (!dest.exists()) {
java.nio.file.Files.write(dest.toPath(),
java.util.Arrays.copyOfRange(data, jpegPos, jpegPos + size));
LOG.info("[CharImport] JPEG vorab extrahiert: '{}' ({} B)", decodedName, size);
any = true;
}
}
// ── PNG: PNG-Magic → IEND ──────────────────────────────────────────
searchPos = 0;
while (true) {
int pngPos = binaryIndexOf(data, pngMagic, searchPos);
if (pngPos < 0) break;
searchPos = pngPos + 1;
int endPos = binaryIndexOf(data, iend, pngPos + 8);
if (endPos < 0) continue;
int size = endPos + iend.length - pngPos;
if (size < 256) continue;
String decodedName = findImageFilenameBeforePos(data, pngPos, new String[]{".png"});
if (decodedName == null) decodedName = "texture_" + pngPos + ".png";
java.io.File dest = new java.io.File(tempDir, decodedName);
if (!dest.exists()) {
java.nio.file.Files.write(dest.toPath(),
java.util.Arrays.copyOfRange(data, pngPos, pngPos + size));
LOG.info("[CharImport] PNG vorab extrahiert: '{}' ({} B)", decodedName, size);
any = true;
}
}
return any ? tempDir : null;
} catch (Exception e) {
LOG.warn("[CharImport] Pre-Load-Texturextraktion fehlgeschlagen: {}", e.getMessage());
return null;
}
}
/**
* Sucht rückwärts ab {@code beforePos} (max 65536 Bytes) nach einem
* Dateinamen-String mit einer der angegebenen Endungen und gibt den
* URL-dekodierten Basis-Dateinamen zurück.
*/
private static String findImageFilenameBeforePos(byte[] data, int beforePos, String[] extensions) {
int scanFrom = Math.max(0, beforePos - 65536);
int bestPos = -1;
String bestExt = null;
for (String ext : extensions) {
byte[] extBytes = ext.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
int pos = scanFrom;
while (pos < beforePos) {
int found = -1;
outer:
for (int i = pos, max = beforePos - extBytes.length; i <= max; i++) {
for (int j = 0; j < extBytes.length; j++) {
if (data[i + j] != extBytes[j]) continue outer;
}
found = i;
break;
}
if (found < 0) break;
if (found > bestPos) { bestPos = found; bestExt = ext; }
pos = found + 1;
}
}
if (bestPos < 0 || bestExt == null) return null;
// Rückwärts zum Anfang des Dateinamens (Stopp bei /, \ oder Non-Printable)
int nameStart = bestPos - 1;
while (nameStart > 0 && data[nameStart] >= 0x20
&& data[nameStart] != '/' && data[nameStart] != '\\') {
nameStart--;
}
nameStart++;
String raw = new String(data, nameStart, bestPos + bestExt.length() - nameStart,
java.nio.charset.StandardCharsets.US_ASCII);
try {
return new java.io.File(java.net.URLDecoder.decode(raw, "UTF-8")).getName();
} catch (Exception e) {
return new java.io.File(raw.replace("+", " ")).getName();
}
}
private static int binaryIndexOf(byte[] data, byte[] pattern, int from) {
outer:
for (int i = from, max = data.length - pattern.length; i <= max; i++) {

View File

@@ -59,6 +59,7 @@ public class ContentController {
private de.blight.editor.ui.RoutineEditorView routineEditorView;
private de.blight.editor.ui.MonologueEditorView monologueEditorView;
private StackPane routineJmeSlot;
private boolean loadingCharacter = false;
// ── Character-Formular-Felder ──────────────────────────────────────────────
private VBox charEditContainer;
@@ -69,7 +70,9 @@ public class ContentController {
private CheckBox charTraderCheck;
private ListView<String> charTraderItemsView;
private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy;
private TextField charNameField, charIdField;
private TextField charMaxHpField, charMaxManaField, charMaxStaminaField;
private ComboBox<String> charWeaponCombo, charShieldCombo;
private TextField charIdField;
private ComboBox<String> charTypeCombo, charModelCombo, charAnimSetCombo;
private ComboBox<de.blight.common.model.Fraction> charFractionCombo;
private Label charEditorStatusLabel;
@@ -263,11 +266,11 @@ public class ContentController {
public void loadCharacterById(String id) {
if (id == null || id.isBlank()) return;
Path charDir = ctx.assetRoot().resolve("Characters");
loadingCharacter = true;
try {
de.blight.common.model.GameCharacter c =
de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character"));
if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : "");
if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : "");
if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : "");
if (charTypeCombo != null)
charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC");
if (charModelCombo != null) charModelCombo.setValue(c.getModelPath());
@@ -303,6 +306,17 @@ public class ContentController {
if (charMsgNeutral != null) charMsgNeutral.setText(msgKey(msgs, de.blight.common.model.Status.NEUTRAL));
if (charMsgEnraged != null) charMsgEnraged.setText(msgKey(msgs, de.blight.common.model.Status.ENRAGED));
if (charMsgEnemy != null) charMsgEnemy.setText(msgKey(msgs, de.blight.common.model.Status.ENEMY));
if (charWeaponCombo != null) {
String wid = npc.getEquippedWeaponId();
charWeaponCombo.setValue(wid != null ? wid : "");
}
if (charShieldCombo != null) {
String sid = npc.getEquippedShieldId();
charShieldCombo.setValue(sid != null ? sid : "");
}
if (charMaxHpField != null) charMaxHpField.setText(String.valueOf(npc.getMaxHp() > 0 ? npc.getMaxHp() : 100));
if (charMaxManaField != null) charMaxManaField.setText(String.valueOf(npc.getMaxMana() > 0 ? npc.getMaxMana() : 100));
if (charMaxStaminaField != null) charMaxStaminaField.setText(String.valueOf(npc.getMaxStamina() > 0 ? npc.getMaxStamina() : 100));
if (dialogEditorView != null) {
dialogEditorView.setDisable(false);
dialogEditorView.loadNpc(npc);
@@ -325,6 +339,8 @@ public class ContentController {
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
} catch (Exception e) {
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage());
} finally {
loadingCharacter = false;
}
}
@@ -923,13 +939,25 @@ public class ContentController {
dialogEditorView = new de.blight.editor.ui.DialogEditorView();
dialogEditorView.setDisable(true);
monologueEditorView = new de.blight.editor.ui.MonologueEditorView();
routineEditorView = new de.blight.editor.ui.RoutineEditorView(ctx.input(), ctx.assetRoot().resolve("Characters"));
routineEditorView.setDisable(true);
routineJmeSlot = new StackPane();
routineJmeSlot.setMinWidth(0);
routineJmeSlot.getChildren().add(ctx.viewportSupplier().get());
SplitPane routineSplit = new SplitPane(routineEditorView, routineJmeSlot);
routineSplit.setDividerPositions(0.25);
routineSplit.widthProperty().addListener((obs, ov, nv) -> {
if (nv.doubleValue() > 0) routineSplit.setDividerPositions(0.25);
});
TabPane tabs = new TabPane();
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
Tab charTab = new Tab("Charakter", buildCharacterSettingsPane());
Tab dialogTab = new Tab("Dialog", dialogEditorView);
Tab monologTab = new Tab("Monologe", monologueEditorView);
tabs.getTabs().addAll(charTab, dialogTab, monologTab);
Tab charTab = new Tab("Charakter", buildCharacterSettingsPane());
Tab dialogTab = new Tab("Dialog", dialogEditorView);
Tab routineTab = new Tab("Tagesabläufe", routineSplit);
Tab monologTab = new Tab("Monologe", monologueEditorView);
tabs.getTabs().addAll(charTab, dialogTab, routineTab, monologTab);
tabs.getSelectionModel().select(charTab);
setContentCenter(tabs);
@@ -1085,9 +1113,6 @@ public class ContentController {
charIdField = new TextField();
charIdField.setPromptText("eindeutige ID (Dateiname)");
charNameField = new TextField();
charNameField.setPromptText("Anzeigename");
charNameField.setEditable(false);
charTypeCombo = new ComboBox<>();
charTypeCombo.getItems().addAll("MainCharacter", "NPC");
@@ -1113,9 +1138,8 @@ public class ContentController {
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
charEditContainer.getChildren().addAll(
dl.apply("ID:"), charIdField,
dl.apply("Name:"), charNameField,
dl.apply("Typ:"), charTypeCombo
dl.apply("ID:"), charIdField,
dl.apply("Typ:"), charTypeCombo
);
charFractionCombo = new ComboBox<>();
@@ -1180,6 +1204,40 @@ public class ContentController {
charMsgEnemy = new TextField();
charMsgEnemy.setPromptText("TextReference-Schlüssel");
// Weapon / shield combos – populated from items directory
charWeaponCombo = new ComboBox<>();
charWeaponCombo.setMaxWidth(Double.MAX_VALUE);
charWeaponCombo.setPromptText("— keine Waffe —");
charWeaponCombo.getItems().add("");
charShieldCombo = new ComboBox<>();
charShieldCombo.setMaxWidth(Double.MAX_VALUE);
charShieldCombo.setPromptText("— kein Schild —");
charShieldCombo.getItems().add("");
try {
java.util.List<de.blight.common.model.Item> allItems =
de.blight.common.model.ItemIO.loadAll(ctx.assetRoot().resolve("items"));
allItems.stream()
.filter(i -> i.getCategory() == de.blight.common.model.ItemCategory.WEAPON
&& i.getSubCategory() != de.blight.common.model.ItemSubCategory.SHIELD)
.map(de.blight.common.model.Item::getItemId)
.filter(id -> id != null && !id.isBlank())
.forEach(charWeaponCombo.getItems()::add);
allItems.stream()
.filter(i -> i.getSubCategory() == de.blight.common.model.ItemSubCategory.SHIELD)
.map(de.blight.common.model.Item::getItemId)
.filter(id -> id != null && !id.isBlank())
.forEach(charShieldCombo.getItems()::add);
} catch (Exception ignored) {}
charMaxHpField = new TextField("100");
charMaxHpField.setPromptText("100");
charMaxManaField = new TextField("100");
charMaxManaField.setPromptText("100");
charMaxStaminaField = new TextField("100");
charMaxStaminaField.setPromptText("100");
charNpcSection = new VBox(4,
dl.apply("Status:"), charStatusCombo,
dl.apply("Handel:"), charTraderCheck,
@@ -1188,7 +1246,14 @@ public class ContentController {
dl.apply("Friendly:"), charMsgFriendly,
dl.apply("Neutral:"), charMsgNeutral,
dl.apply("Enraged:"), charMsgEnraged,
dl.apply("Enemy:"), charMsgEnemy);
dl.apply("Enemy:"), charMsgEnemy,
dsl.apply("Basiswerte"),
dl.apply("Max HP:"), charMaxHpField,
dl.apply("Max Mana:"), charMaxManaField,
dl.apply("Max Ausdauer:"), charMaxStaminaField,
dsl.apply("Ausrüstung"),
dl.apply("Primärwaffe:"), charWeaponCombo,
dl.apply("Schild (nur bei Schwert):"), charShieldCombo);
boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
charNpcSection.setVisible(npcInitial);
charNpcSection.setManaged(npcInitial);
@@ -1238,17 +1303,11 @@ public class ContentController {
);
// Buttons
Button saveBtn = new Button("Speichern");
Button clearBtn = new Button("Leeren");
saveBtn.setMaxWidth(Double.MAX_VALUE);
clearBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-background-color:#2a5a2a;-fx-text-fill:white;");
saveBtn.setOnAction(e -> saveCharacter(charDir));
clearBtn.setOnAction(e -> clearCharacterForm());
HBox.setHgrow(saveBtn, Priority.ALWAYS);
HBox.setHgrow(clearBtn, Priority.ALWAYS);
HBox btnBar = new HBox(6, saveBtn, clearBtn);
charEditContainer.getChildren().addAll(new Separator(), btnBar);
charEditContainer.getChildren().addAll(new Separator(), clearBtn);
// Auto-Save-Listener
charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
@@ -1264,6 +1323,12 @@ public class ContentController {
charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
charWeaponCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir));
charShieldCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir));
charMaxHpField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
charMaxManaField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
charMaxStaminaField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
if (routineEditorView != null) routineEditorView.setOnDataChanged(() -> autoSaveCharacter(charDir));
inner.getChildren().add(charEditContainer);
@@ -1275,6 +1340,7 @@ public class ContentController {
}
private void autoSaveCharacter(Path charDir) {
if (loadingCharacter) return;
if (charIdField == null || charIdField.getText().trim().isBlank()) return;
saveCharacter(charDir);
}
@@ -1333,8 +1399,7 @@ public class ContentController {
}
private void clearCharacterForm() {
if (charIdField != null) charIdField.clear();
if (charNameField != null) charNameField.clear();
if (charIdField != null) charIdField.clear();
if (charTypeCombo != null) charTypeCombo.setValue("NPC");
if (charModelCombo != null) charModelCombo.setValue(null);
if (charAnimSetCombo != null) charAnimSetCombo.setValue(null);
@@ -1373,8 +1438,6 @@ public class ContentController {
? new de.blight.common.model.MainCharacter()
: new de.blight.common.model.NPC();
c.setCharacterId(id);
String nameId = charNameField != null ? charNameField.getText().trim() : "";
c.setName(nameId.isBlank() ? null : new de.blight.common.model.TextReference(nameId));
c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null);
c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null);
if (c instanceof de.blight.common.model.NPC npc) {
@@ -1404,12 +1467,24 @@ public class ContentController {
} else {
npc.setItems(null);
}
String wid = charWeaponCombo != null ? charWeaponCombo.getValue() : null;
npc.setEquippedWeaponId(wid != null && !wid.isBlank() ? wid : null);
String sid = charShieldCombo != null ? charShieldCombo.getValue() : null;
npc.setEquippedShieldId(sid != null && !sid.isBlank() ? sid : null);
npc.setMaxHp(parseStatField(charMaxHpField, 100));
npc.setMaxMana(parseStatField(charMaxManaField, 100));
npc.setMaxStamina(parseStatField(charMaxStaminaField, 100));
}
if (c instanceof de.blight.common.model.MainCharacter mc) {
mc.setAbilities(buildAbilities());
}
try {
de.blight.common.model.CharacterIO.save(c, charDir);
if (c instanceof de.blight.common.model.NPC) {
String langKey = "npc." + id + ".name";
String langName = deriveDisplayName(id);
de.blight.editor.ui.LangDefaults.writeIfAbsent(langKey, langName);
}
refreshContentCharacterTree();
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
} catch (Exception e) {
@@ -1417,6 +1492,27 @@ public class ContentController {
}
}
private static int parseStatField(TextField field, int defaultVal) {
if (field == null) { return defaultVal; }
try {
int v = Integer.parseInt(field.getText().trim());
return v > 0 ? v : defaultVal;
} catch (NumberFormatException e) {
return defaultVal;
}
}
private static String deriveDisplayName(String id) {
String s = id.replaceAll("[0-9]", "").replace('_', ' ').replace('-', ' ').trim();
StringBuilder sb = new StringBuilder();
for (String word : s.split(" +")) {
if (word.isEmpty()) continue;
if (sb.length() > 0) sb.append(' ');
sb.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
}
return sb.isEmpty() ? id : sb.toString();
}
private void refreshFractionCombo() {
loadedFractions.clear();
loadedFractions.addAll(

View File

@@ -268,6 +268,7 @@ public class GameController {
javaExe,
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
"-Xmx2g",
"-Djava.library.path=" + libPath,
"-Dblight.project.root=" + projRoot,
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,

View File

@@ -69,7 +69,12 @@ public class RoutineEditorView extends VBox {
private final List<PlacedItem> placedItems = new ArrayList<>();
private final List<String> npcIds = new ArrayList<>();
private boolean updatingForm = false;
private boolean updatingForm = false;
private Runnable onDataChanged = null;
public void setOnDataChanged(Runnable r) { this.onDataChanged = r; }
private void fireDataChanged() { if (onDataChanged != null) onDataChanged.run(); }
// ── Polling-Timer ──────────────────────────────────────────────────────────
@@ -358,6 +363,7 @@ public class RoutineEditorView extends VBox {
activeBlock.getActivity().getWaypoints().add(wp);
refreshWaypointList();
updateMarkers();
fireDataChanged();
}));
delWp.setOnAction(e -> {
@@ -366,6 +372,7 @@ public class RoutineEditorView extends VBox {
wps.remove(wps.size() - 1);
refreshWaypointList();
updateMarkers();
fireDataChanged();
}
});
@@ -408,6 +415,7 @@ public class RoutineEditorView extends VBox {
activeBlock.setStartHour(startSpin.getValue());
activeBlock.setEndHour(endSpin.getValue());
refreshCoverage();
fireDataChanged();
}
private void applyActivityToBlock() {
@@ -456,6 +464,7 @@ public class RoutineEditorView extends VBox {
}
// PATROL is managed directly in buildPatrolFields
}
fireDataChanged();
}
// ── Routine-Operationen ────────────────────────────────────────────────────
@@ -470,6 +479,7 @@ public class RoutineEditorView extends VBox {
routines.add(r);
refreshRoutineList();
routineList.getSelectionModel().selectLast();
fireDataChanged();
});
}
@@ -482,6 +492,7 @@ public class RoutineEditorView extends VBox {
if (!name.isBlank()) {
activeRoutine.setName(name.trim());
refreshRoutineList();
fireDataChanged();
}
});
}
@@ -494,6 +505,7 @@ public class RoutineEditorView extends VBox {
refreshRoutineList();
if (!routines.isEmpty())
routineList.getSelectionModel().select(Math.min(idx, routines.size() - 1));
fireDataChanged();
}
private void addBlock() {
@@ -502,6 +514,7 @@ public class RoutineEditorView extends VBox {
activeRoutine.getBlocks().add(b);
refreshBlockList();
blockList.getSelectionModel().selectLast();
fireDataChanged();
}
private void deleteBlock() {
@@ -514,6 +527,7 @@ public class RoutineEditorView extends VBox {
refreshCoverage();
if (!activeRoutine.getBlocks().isEmpty())
blockList.getSelectionModel().select(Math.min(idx, activeRoutine.getBlocks().size() - 1));
fireDataChanged();
}
private void moveBlock(int delta) {