Compare commits

..

2 Commits

Author SHA1 Message Date
974ee5765e Ein wenig an der GUI gebastetl 2026-08-19 22:21:32 +02:00
c221c26a23 Weiter Ingame an den animationen gepfeilt 2026-08-19 19:37:59 +02:00
9 changed files with 184 additions and 94 deletions

View File

@@ -2,35 +2,27 @@
"chapter": 0, "chapter": 0,
"level": 0, "level": 0,
"xp": 0, "xp": 0,
"currentHp": 100, "currentHp": 0,
"currentStamina": 100, "currentStamina": 0,
"currentMana": 100, "currentMana": 0,
"maxHp": 100, "maxHp": 0,
"maxStamina": 100, "maxStamina": 0,
"maxMana": 100, "maxMana": 0,
"openHpRegeneration": 0, "openHpRegeneration": 0,
"openManaRegeneration": 0, "openManaRegeneration": 0,
"openStaminaRegeneration": 0, "openStaminaRegeneration": 0,
"restorationValue": 10, "restorationValue": 10,
"abilities": { "playedMonologueIds": [],
"lvlMagic": 1, "firedTriggerIds": [],
"lvlStaffCombat": 1, "visitedZoneIds": [],
"lvlSwordsmanship": 1, "gameVariables": {},
"lvlArchery": 1, "memberFractionIds": [],
"lvlHeavyWeapons": 1,
"lvlCrossbow": 1,
"lvlThievery": 0,
"lvlAlchemy": 0,
"lvlEngineering": 0,
"lvlSmithery": 0,
"lvlEnchanting": 0
},
"listeners": [], "listeners": [],
"characterId": "hero", "characterId": "hero",
"name": { "name": {
"id": "hero.name" "id": "hero.name"
}, },
"modelPath": "Models/Chars/mainchar.j3o", "modelPath": "Characters/Models/hero.j3o",
"animSetPath": "human", "animSetPath": "human",
"type": "MAIN_CHARACTER" "type": "MAIN_CHARACTER"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -11326,8 +11326,10 @@ public class EditorApp extends Application {
var clip = clipCombo.getValue(); var clip = clipCombo.getValue();
if (action != null && clip != null) { if (action != null && clip != null) {
String entry = action.name() + " → " + clip; String entry = action.name() + " → " + clip;
if (!animSetActionListView.getItems().contains(entry)) if (!animSetActionListView.getItems().contains(entry)) {
animSetActionListView.getItems().add(entry); animSetActionListView.getItems().add(entry);
autoSaveAnimSet();
}
} }
}); });
} }
@@ -11410,6 +11412,11 @@ public class EditorApp extends Application {
saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir); saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir);
} }
private void autoSaveCharacter() {
if (charIdField == null || charIdField.getText().trim().isBlank()) return;
saveCharacter(ASSET_ROOT.resolve("Characters"));
}
private void saveCurrentAnimSet(String setName, Path setDir) { private void saveCurrentAnimSet(String setName, Path setDir) {
if (animSetClipListView == null) { if (animSetClipListView == null) {
return; return;
@@ -12037,16 +12044,6 @@ public class EditorApp extends Application {
VBox inner = new VBox(8); VBox inner = new VBox(8);
inner.setPadding(new Insets(10)); inner.setPadding(new Insets(10));
Button newCharBtn = new Button("Neuer Charakter");
newCharBtn.setMaxWidth(Double.MAX_VALUE);
newCharBtn.setOnAction(e -> clearCharacterForm());
Button importCharModelBtn = new Button("Character-Modell importieren (FBX)…");
importCharModelBtn.setMaxWidth(Double.MAX_VALUE);
importCharModelBtn.setStyle("-fx-font-weight: bold;");
importCharModelBtn.setOnAction(e -> handleCharacterModelImport(importCharModelBtn.getScene().getWindow()));
inner.getChildren().addAll(newCharBtn, importCharModelBtn);
charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen"); charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen");
charEditorStatusLabel.setWrapText(true); charEditorStatusLabel.setWrapText(true);
@@ -12097,18 +12094,6 @@ public class EditorApp extends Application {
refreshCharAnimSetCombo(); refreshCharAnimSetCombo();
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet()); charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
Button stripClipsBtn = new Button("Eingebettete Clips löschen");
stripClipsBtn.setMaxWidth(Double.MAX_VALUE);
stripClipsBtn.setDisable(true);
charModelCombo.valueProperty().addListener((obs, ov, nv) ->
stripClipsBtn.setDisable(nv == null || nv.isBlank()));
stripClipsBtn.setOnAction(e -> {
String modelPath = charModelCombo.getValue();
if (modelPath == null || modelPath.isBlank()) return;
if (charEditorStatusLabel != null)
charEditorStatusLabel.setText("Entferne eingebettete Clips…");
input.animStripClipsRequest.set(modelPath);
});
charEditContainer.getChildren().addAll( charEditContainer.getChildren().addAll(
dl.apply("ID:"), charIdField, dl.apply("ID:"), charIdField,
@@ -12202,8 +12187,7 @@ public class EditorApp extends Application {
charEditContainer.getChildren().add(charNpcSection); charEditContainer.getChildren().add(charNpcSection);
charEditContainer.getChildren().addAll( charEditContainer.getChildren().addAll(
dl.apply("Modell:"), charModelCombo, dl.apply("Modell:"), charModelCombo,
dl.apply("Anim-Set:"), charAnimSetCombo, dl.apply("Anim-Set:"), charAnimSetCombo
stripClipsBtn
); );
// ── Aktions-Zuweisung ───────────────────────────────────────────────── // ── Aktions-Zuweisung ─────────────────────────────────────────────────
@@ -12214,13 +12198,20 @@ public class EditorApp extends Application {
charActionLabelsBox.getChildren().add(actionHint); charActionLabelsBox.getChildren().add(actionHint);
charEditContainer.getChildren().add(charActionLabelsBox); charEditContainer.getChildren().add(charActionLabelsBox);
// ── Speichern ───────────────────────────────────────────────────────── // ── Auto-Save-Listener ────────────────────────────────────────────────
charEditContainer.getChildren().add(new Separator()); charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
Button saveCharBtn = new Button("Charakter speichern"); charTypeCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
saveCharBtn.setMaxWidth(Double.MAX_VALUE); charModelCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
saveCharBtn.setStyle("-fx-font-weight:bold;-fx-background-color:#3a7a3a;-fx-text-fill:white;"); charAnimSetCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
saveCharBtn.setOnAction(e -> saveCharacter(charDir)); charFractionCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
charEditContainer.getChildren().add(saveCharBtn); charStatusCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
charTraderCheck.selectedProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
charTraderItemsView.getItems().addListener(
(javafx.collections.ListChangeListener<String>) c -> autoSaveCharacter());
charMsgFriendly.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
inner.getChildren().add(charEditContainer); inner.getChildren().add(charEditContainer);

View File

@@ -336,21 +336,14 @@ public class AnimationLibrary extends BaseAppState {
return null; return null;
} }
// Clips mit Vorwärts-Root-Motion in Local-Y (rennen, gehen, springen):
// Y wird auf 0 eingefroren → kein Drift. Alle anderen Clips: Y bleibt frei (Hinsetzen usw.)
private static final java.util.Set<String> LOCOMOTION_CLIPS = java.util.Set.of(
"running", "walking", "sprinting", "running_jump"
);
/** /**
* Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips). * Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips).
* Nur dieser eine Bone wird modifiziert — alle anderen Bones bleiben unverändert. * Nur dieser eine Bone wird modifiziert — alle anderen Bones bleiben unverändert.
* *
* In diesen Mixamo-Exporten ist die Achsenbelegung des Hips-Bones: * Achsenbelegung dieses Mixamo-Exports im Hips-Bone-Local-Space:
* Local X → seitlich → wird auf 0 eingefroren (kein Seiten-Drift) * Local X → JME3-X (seitlich) → wird auf 0 eingefroren
* Local Y → vorwärts → wird auf 0 eingefroren für Lauf-Clips (kein Vorwärts-Drift) * Local Y → JME3-Y (Höhe oben) → IMMER frei lassen (Charakter-Höhe erhalten)
* bleibt frei für Sitz/Stand-Clips (leichte Neigungsbewegung) * Local Z → JME3-Z (vorwärts) → wird auf 0 eingefroren (alle Clips auf der Stelle)
* Local Z → Höhe → IMMER frei lassen (Charakter-Höhe und Setz-Bewegung erhalten)
*/ */
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) { public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
@@ -367,8 +360,6 @@ public class AnimationLibrary extends BaseAppState {
} }
if (minDepth == Integer.MAX_VALUE) return clip; if (minDepth == Integer.MAX_VALUE) return clip;
boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName().toLowerCase(java.util.Locale.ROOT));
List<AnimTrack<?>> newTracks = new ArrayList<>(); List<AnimTrack<?>> newTracks = new ArrayList<>();
boolean modified = false; boolean modified = false;
for (AnimTrack<?> track : clip.getTracks()) { for (AnimTrack<?> track : clip.getTracks()) {
@@ -383,11 +374,6 @@ public class AnimationLibrary extends BaseAppState {
continue; continue;
} }
// Y-Normalisierung für Nicht-Lauf-Clips: Frame-0 auf Bind-Pose-Y
float bindY = j.getInitialTransform().getTranslation().y;
float frame0Y = translations[0].y;
float yOffset = bindY - frame0Y;
// Diagnostik // Diagnostik
float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE; float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE;
float xRange = 0, zRange = 0; float xRange = 0, zRange = 0;
@@ -397,21 +383,18 @@ public class AnimationLibrary extends BaseAppState {
xRange = Math.max(xRange, Math.abs(t.x - translations[0].x)); xRange = Math.max(xRange, Math.abs(t.x - translations[0].x));
zRange = Math.max(zRange, Math.abs(t.z - translations[0].z)); zRange = Math.max(zRange, Math.abs(t.z - translations[0].z));
} }
log.info("[AnimLib] snap '{}' root='{}' locomotion={} bindY={} frame0Y={} yOffset={} yRange={} xRange={} zRange={}", log.info("[AnimLib] snap '{}' root='{}' xRange={} yRange={} zRange={}",
clip.getName(), j.getName(), isLocomotion, clip.getName(), j.getName(),
String.format("%.3f", bindY), String.format("%.3f", frame0Y), String.format("%.3f", xRange),
String.format("%.3f", yOffset), String.format("%.3f", yMax - yMin), String.format("%.3f", yMax - yMin),
String.format("%.3f", xRange), String.format("%.3f", zRange)); String.format("%.3f", zRange));
Vector3f[] snapped = new Vector3f[translations.length]; Vector3f[] snapped = new Vector3f[translations.length];
for (int i = 0; i < translations.length; i++) { for (int i = 0; i < translations.length; i++) {
float newY = isLocomotion
? 0f // Lauf-Clips: Y einfrieren (Vorwärts-Drift weg)
: (translations[i].y + yOffset); // Rest: Y normalisiert (Neigung erhalten)
snapped[i] = new Vector3f( snapped[i] = new Vector3f(
0f, // X immer 0 (Seiten-Drift weg) 0f, // X = 0 (kein seitlicher Drift)
newY, translations[i].y, // Y frei (Höhe; Y=oben in bone-local / JME3-Y)
translations[i].z // Z immer frei (Höhe und Setz-Bewegung erhalten) 0f // Z = 0 (kein Vorwärts-Drift; Z=vorwärts in bone-local)
); );
} }
newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales())); newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales()));

View File

@@ -18,6 +18,12 @@ import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.shape.Quad; import com.jme3.scene.shape.Quad;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import de.blight.game.post.GaussianBlurFilter; import de.blight.game.post.GaussianBlurFilter;
import de.blight.game.scene.WorldScene; import de.blight.game.scene.WorldScene;
@@ -65,8 +71,15 @@ public abstract class MenuScreen extends BaseAppState {
private final String MAP_CLICK = "_MsClick_" + System.identityHashCode(this); private final String MAP_CLICK = "_MsClick_" + System.identityHashCode(this);
private record Btn(float x, float y, float w, float h, Runnable action) {} private record Btn(float x, float y, float w, float h, Runnable action,
Node patchNode, BitmapText label) {}
private final List<Btn> buttons = new ArrayList<>(); private final List<Btn> buttons = new ArrayList<>();
private Btn pressedBtn = null;
private Btn hoveredBtn = null;
private Node hoverGlow = null;
private Texture2D glowTex = null;
private static final float PRESS_DX = 1f, PRESS_DY = -2f;
private static final float GLOW_PAD = 20f;
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -114,6 +127,9 @@ public abstract class MenuScreen extends BaseAppState {
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
panel = null; panel = null;
buttons.clear(); buttons.clear();
pressedBtn = null;
hoveredBtn = null;
if (hoverGlow != null) { hoverGlow.removeFromParent(); hoverGlow = null; }
if (withBlur()) removeBlur(); if (withBlur()) removeBlur();
} }
@@ -171,7 +187,8 @@ public abstract class MenuScreen extends BaseAppState {
case ARROW -> NinePatch.buttonArrow(assets); case ARROW -> NinePatch.buttonArrow(assets);
default -> NinePatch.button(assets); default -> NinePatch.button(assets);
}; };
parent.attachChild(patch.build(x, y, w, h, 0f)); Node patchNode = patch.build(x, y, w, h, 0f);
parent.attachChild(patchNode);
ColorRGBA col = (style == BtnStyle.DISABLED) ? UiTheme.COL_MUTED : UiTheme.COL_WHITE; ColorRGBA col = (style == BtnStyle.DISABLED) ? UiTheme.COL_MUTED : UiTheme.COL_WHITE;
BitmapText lbl = crispText(label, UiTheme.FONT_LABEL, col); BitmapText lbl = crispText(label, UiTheme.FONT_LABEL, col);
@@ -180,23 +197,110 @@ public abstract class MenuScreen extends BaseAppState {
parent.attachChild(lbl); parent.attachChild(lbl);
if (style != BtnStyle.DISABLED && action != null) { if (style != BtnStyle.DISABLED && action != null) {
buttons.add(new Btn(x, y, w, h, action)); buttons.add(new Btn(x, y, w, h, action, patchNode, lbl));
} }
} }
// ── Update-Loop (Hover) ───────────────────────────────────────────────────
@Override
public void update(float tpf) {
if (buttons.isEmpty() || panel == null) return;
float[] v = toVirtual(app.getInputManager().getCursorPosition());
Btn hovered = null;
for (Btn b : buttons) {
if (v[0] >= b.x() && v[0] <= b.x() + b.w()
&& v[1] >= b.y() && v[1] <= b.y() + b.h()) {
hovered = b;
break;
}
}
if (hovered != hoveredBtn) {
if (hoverGlow != null) { hoverGlow.removeFromParent(); hoverGlow = null; }
hoveredBtn = hovered;
if (hoveredBtn != null) {
hoverGlow = buildHoverGlow(hoveredBtn);
panel.attachChild(hoverGlow);
}
}
if (hoverGlow != null) {
float dx = (pressedBtn == hoveredBtn) ? PRESS_DX : 0f;
float dy = (pressedBtn == hoveredBtn) ? PRESS_DY : 0f;
hoverGlow.setLocalTranslation(hoveredBtn.x() + dx, hoveredBtn.y() + dy, 0f);
}
}
private Node buildHoverGlow(Btn b) {
Geometry q = new Geometry("glow",
new Quad(b.w() + GLOW_PAD * 2f, b.h() + GLOW_PAD * 2f));
Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
m.setTexture("ColorMap", getGlowTex());
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive);
q.setQueueBucket(RenderQueue.Bucket.Gui);
q.setMaterial(m);
// -GLOW_PAD damit der Glow um den Button herausragt; Node selbst wird auf Button-Pos gesetzt
q.setLocalTranslation(-GLOW_PAD, -GLOW_PAD, -0.5f);
Node g = new Node("hover-glow");
g.attachChild(q);
return g;
}
private Texture2D getGlowTex() {
if (glowTex != null) return glowTex;
int W = 64, H = 64;
ByteBuffer buf = BufferUtils.createByteBuffer(W * H * 4);
for (int py = 0; py < H; py++) {
for (int px = 0; px < W; px++) {
float u = (px + 0.5f) / W;
float v = (py + 0.5f) / H;
float a = glowFade(u) * glowFade(v);
buf.put((byte) 13); // R ≈ 0.05 × 255
buf.put((byte) 255); // G = neon
buf.put((byte) 51); // B ≈ 0.20 × 255
buf.put((byte)(int)(a * 220f)); // A peak ≈ 86 %
}
}
buf.flip();
glowTex = new Texture2D(new Image(Image.Format.RGBA8, W, H, buf, ColorSpace.Linear));
glowTex.setMagFilter(Texture.MagFilter.Bilinear);
glowTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
return glowTex;
}
private static float glowFade(float t) {
float d = Math.abs(t - 0.5f) * 2f; // 0 = Mitte, 1 = Rand
float x = 1f - Math.min(1f, d);
return x * x * (3f - 2f * x); // smoothstep
}
// ── Klick-Listener ──────────────────────────────────────────────────────── // ── Klick-Listener ────────────────────────────────────────────────────────
private final ActionListener clickListener = (name, pressed, tpf) -> { private final ActionListener clickListener = (name, pressed, tpf) -> {
if (!pressed) return;
float[] v = toVirtual(app.getInputManager().getCursorPosition()); float[] v = toVirtual(app.getInputManager().getCursorPosition());
if (pressed) {
onScreenClick(v); onScreenClick(v);
for (Btn b : buttons) { for (Btn b : buttons) {
if (v[0] >= b.x() && v[0] <= b.x() + b.w() if (v[0] >= b.x() && v[0] <= b.x() + b.w()
&& v[1] >= b.y() && v[1] <= b.y() + b.h()) { && v[1] >= b.y() && v[1] <= b.y() + b.h()) {
b.action().run(); pressedBtn = b;
b.patchNode().move(PRESS_DX, PRESS_DY, 0f);
b.label().move(PRESS_DX, PRESS_DY, 0f);
return; return;
} }
} }
} else {
if (pressedBtn != null) {
pressedBtn.patchNode().move(-PRESS_DX, -PRESS_DY, 0f);
pressedBtn.label().move(-PRESS_DX, -PRESS_DY, 0f);
Btn released = pressedBtn;
pressedBtn = null;
// Aktion nur auslösen wenn Maus noch über dem Button
if (v[0] >= released.x() && v[0] <= released.x() + released.w()
&& v[1] >= released.y() && v[1] <= released.y() + released.h()) {
released.action().run();
}
}
}
}; };
/** Hook für Subklassen die auch auf Klicks reagieren (z.B. ConfigScreen Zeilen). */ /** Hook für Subklassen die auch auf Klicks reagieren (z.B. ConfigScreen Zeilen). */

View File

@@ -171,6 +171,12 @@ public class NinePatch {
int t = ty >= 0 ? ty + 1 : h / 4; int t = ty >= 0 ? ty + 1 : h / 4;
int b = by >= 0 ? h - by : h / 4; int b = by >= 0 ? h - by : h / 4;
// Sicherstellen, dass Mittelbereich mindestens 1 Pixel groß ist.
// Fehlerhafte Guide-Erkennung (Pixel mit zufällig passendem Alpha) kann
// sonst l+r >= w oder t+b >= h erzeugen → Endlosschleife in den Tiling-Methoden.
if (l + r >= w) { l = w / 4; r = w / 4; }
if (t + b >= h) { t = h / 4; b = h / 4; }
return new int[]{l, r, t, b}; return new int[]{l, r, t, b};
} }
@@ -182,12 +188,16 @@ public class NinePatch {
* ty, by: Zeilenindizes der oberen/unteren Guide-Linie (PNG-Koordinaten) * ty, by: Zeilenindizes der oberen/unteren Guide-Linie (PNG-Koordinaten)
*/ */
private static void stripGuides(BufferedImage img, int w, int h, int lx, int rx, int ty, int by) { private static void stripGuides(BufferedImage img, int w, int h, int lx, int rx, int ty, int by) {
// Snapshot der Guide-Positionen (vor Modifikation) // Snapshot der Guide-Positionen (vor Modifikation).
// Als Guide zählen: exakte Guide-Alphas ODER semi-transparente Pixel (alpha < 200)
// auf den bekannten Guide-Zeilen/-Spalten — entsteht durch Anti-Aliasing beim Malen.
boolean[][] isGuide = new boolean[h][w]; boolean[][] isGuide = new boolean[h][w];
for (int y = 0; y < h; y++) { for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) { for (int x = 0; x < w; x++) {
int a = (img.getRGB(x, y) >> 24) & 0xff; int a = (img.getRGB(x, y) >> 24) & 0xff;
isGuide[y][x] = GUIDE_ALPHAS.contains(a); boolean exactGuide = GUIDE_ALPHAS.contains(a);
boolean onGuideLine = (x == lx || x == rx || y == ty || y == by);
isGuide[y][x] = exactGuide || (onGuideLine && a < 200 && a > 0);
} }
} }
@@ -332,6 +342,7 @@ public class NinePatch {
float u1, float v1, float u2, float v2, float u1, float v1, float u2, float v2,
float cw, float ch, float cw, float ch,
float texW, float texH, float z) { float texW, float texH, float z) {
if (cw <= 0 || ch <= 0) return;
int iz = 0; int iz = 0;
for (float py = sy1; py < sy2; py += ch, iz++) { for (float py = sy1; py < sy2; py += ch, iz++) {
float th = Math.min(ch, sy2 - py); float th = Math.min(ch, sy2 - py);
@@ -352,6 +363,7 @@ public class NinePatch {
float sx1, float py, float sx2, float ph, float sx1, float py, float sx2, float ph,
float u1, float u2, float vMin, float vMax, float u1, float u2, float vMin, float vMax,
float cw, float texW, float z) { float cw, float texW, float z) {
if (cw <= 0) return;
int ix = 0; int ix = 0;
for (float px = sx1; px < sx2; px += cw, ix++) { for (float px = sx1; px < sx2; px += cw, ix++) {
float tw = Math.min(cw, sx2 - px); float tw = Math.min(cw, sx2 - px);
@@ -366,6 +378,7 @@ public class NinePatch {
float px, float sy1, float pw, float sy2, float px, float sy1, float pw, float sy2,
float uMin, float uMax, float v1, float v2, float uMin, float uMax, float v1, float v2,
float ch, float texH, float z) { float ch, float texH, float z) {
if (ch <= 0) return;
int iz = 0; int iz = 0;
for (float py = sy1; py < sy2; py += ch, iz++) { for (float py = sy1; py < sy2; py += ch, iz++) {
float th = Math.min(ch, sy2 - py); float th = Math.min(ch, sy2 - py);

View File

@@ -168,7 +168,7 @@ public class PlayerInputControl {
visualBaseTranslation = visual.getLocalTranslation().clone(); visualBaseTranslation = visual.getLocalTranslation().clone();
} }
try { try {
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName); AnimSet as = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
animOffsets = as.getAnimOffsets(); animOffsets = as.getAnimOffsets();
log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size()); log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size());
} catch (Exception e) { } catch (Exception e) {
@@ -629,6 +629,7 @@ public class PlayerInputControl {
} }
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action); String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName); log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
if (runningClip != null) nextTransitionLength = 0.15;
if (clip != null && tryPlay(clip)) return; if (clip != null && tryPlay(clip)) return;
if (action != AnimationAction.DEFAULT) { if (action != AnimationAction.DEFAULT) {
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT); String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);

View File

@@ -277,6 +277,12 @@ public class CharacterNavigator {
} }
if (clip == null) return; if (clip == null) return;
if (!animLib.applyTo(clip, visual)) return; if (!animLib.applyTo(clip, visual)) return;
if (currentAnim != null) {
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction ba) {
ba.setTransitionLength(0.15);
}
}
com.jme3.anim.tween.action.Action act = animComposer.setCurrentAction(clip); com.jme3.anim.tween.action.Action act = animComposer.setCurrentAction(clip);
if (act == null) return; if (act == null) return;
if (skinningControl != null && !skinningControl.isEnabled()) { if (skinningControl != null && !skinningControl.isEnabled()) {