Begonnen: Verhalten von Gegnern bei annäherung, Verfolgung etc
This commit is contained in:
@@ -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++) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user