Beginn des Kampf Systems
This commit is contained in:
BIN
assets/imported/animations/Sword And Shield Slash.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Slash.fbx
Normal file
Binary file not shown.
BIN
assets/imported/models/rostiges_kurzschwert.glb
Normal file
BIN
assets/imported/models/rostiges_kurzschwert.glb
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
@@ -7,7 +7,9 @@
|
|||||||
"Sprint",
|
"Sprint",
|
||||||
"Stand To Sit",
|
"Stand To Sit",
|
||||||
"T-Pose",
|
"T-Pose",
|
||||||
"Walking"
|
"Walking",
|
||||||
|
"Jumping",
|
||||||
|
"Sword And Shield Slash"
|
||||||
],
|
],
|
||||||
"actionMap": {
|
"actionMap": {
|
||||||
"DEFAULT": "Idle",
|
"DEFAULT": "Idle",
|
||||||
@@ -17,7 +19,10 @@
|
|||||||
"SIT_DOWN": "Stand To Sit",
|
"SIT_DOWN": "Stand To Sit",
|
||||||
"SITTING": "Sitting Idle",
|
"SITTING": "Sitting Idle",
|
||||||
"SIT_UP": "Sit To Stand",
|
"SIT_UP": "Sit To Stand",
|
||||||
"IDLE": "Idle"
|
"IDLE": "Idle",
|
||||||
|
"JUMP": "Jumping",
|
||||||
|
"SWORD_ATTACK_1": "Sword And Shield Slash",
|
||||||
|
"SWORD_ATTACK_2": "Sword And Shield Slash"
|
||||||
},
|
},
|
||||||
"previewModelPath": "Characters/Models/hero.j3o",
|
"previewModelPath": "Characters/Models/hero.j3o",
|
||||||
"animOffsets": {
|
"animOffsets": {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#Tue Sep 15 12:30:48 CEST 2026
|
||||||
|
attachedEmitters.count=0
|
||||||
|
attachedLights.count=0
|
||||||
|
attachedSounds.count=0
|
||||||
|
castShadow=true
|
||||||
|
category=
|
||||||
|
cullDistance=120.0
|
||||||
|
footstepSurface=
|
||||||
|
interactableOffsetX=0.0
|
||||||
|
interactableOffsetY=0.5
|
||||||
|
interactableOffsetZ=0.0
|
||||||
|
interactableRotY=0.0
|
||||||
|
interactableType=NONE
|
||||||
|
lod1Distance=30.0
|
||||||
|
lod1Path=
|
||||||
|
lod2Distance=80.0
|
||||||
|
lod2Path=
|
||||||
|
modelCategory=NONE
|
||||||
|
name=rostiges_kurzschwert
|
||||||
|
pivotOffsetY=0.0
|
||||||
|
placementOffsetY=0.0
|
||||||
|
randomScaleMax=1.0
|
||||||
|
randomScaleMin=1.0
|
||||||
|
receiveShadow=true
|
||||||
|
scaleX=1.0
|
||||||
|
scaleY=1.0
|
||||||
|
scaleZ=1.0
|
||||||
|
solid=true
|
||||||
|
tags=
|
||||||
|
uniformScale=true
|
||||||
Binary file not shown.
@@ -0,0 +1,30 @@
|
|||||||
|
#Tue Sep 15 12:31:37 CEST 2026
|
||||||
|
attachedEmitters.count=0
|
||||||
|
attachedLights.count=0
|
||||||
|
attachedSounds.count=0
|
||||||
|
castShadow=true
|
||||||
|
category=
|
||||||
|
cullDistance=120.0
|
||||||
|
footstepSurface=
|
||||||
|
interactableOffsetX=0.0
|
||||||
|
interactableOffsetY=0.5
|
||||||
|
interactableOffsetZ=0.0
|
||||||
|
interactableRotY=0.0
|
||||||
|
interactableType=NONE
|
||||||
|
lod1Distance=30.0
|
||||||
|
lod1Path=
|
||||||
|
lod2Distance=80.0
|
||||||
|
lod2Path=
|
||||||
|
modelCategory=NONE
|
||||||
|
name=rostiges_kurzschwert
|
||||||
|
pivotOffsetY=0.0
|
||||||
|
placementOffsetY=0.0
|
||||||
|
randomScaleMax=1.0
|
||||||
|
randomScaleMin=1.0
|
||||||
|
receiveShadow=true
|
||||||
|
scaleX=1.0
|
||||||
|
scaleY=1.0
|
||||||
|
scaleZ=1.0
|
||||||
|
solid=false
|
||||||
|
tags=
|
||||||
|
uniformScale=true
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"itemId": "rostiges_kurzschwert",
|
||||||
|
"category": "WEAPON",
|
||||||
|
"subCategory": "SWORD",
|
||||||
|
"name": {
|
||||||
|
"id": "rostiges_kurzschwert.name"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"id": "rostiges_kurzschwert.description"
|
||||||
|
},
|
||||||
|
"worthGold": 0,
|
||||||
|
"damage": 0,
|
||||||
|
"grip": {
|
||||||
|
"posX": 0.24,
|
||||||
|
"posY": 0.05,
|
||||||
|
"posZ": 0.0,
|
||||||
|
"rotX": 0.0,
|
||||||
|
"rotY": -90.0,
|
||||||
|
"rotZ": 0.0
|
||||||
|
},
|
||||||
|
"modelRef": {
|
||||||
|
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
|
||||||
|
},
|
||||||
|
"consumable": false
|
||||||
|
}
|
||||||
@@ -23,6 +23,12 @@ public class SaveGame {
|
|||||||
public float x, y, z;
|
public float x, y, z;
|
||||||
/** itemId → Anzahl */
|
/** itemId → Anzahl */
|
||||||
public Map<String, Integer> inventory = new LinkedHashMap<>();
|
public Map<String, Integer> inventory = new LinkedHashMap<>();
|
||||||
|
/** Ausgerüstete Nahkampfwaffe (itemId, nullable). */
|
||||||
|
public String equippedMeleeWeaponId;
|
||||||
|
/** Ausgerüstete Fernkampfwaffe (itemId, nullable). */
|
||||||
|
public String equippedRangedWeaponId;
|
||||||
|
/** Ausgerüsteter Schild (itemId, nullable). */
|
||||||
|
public String equippedShieldId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class WorldSave {
|
public static class WorldSave {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public class Item implements Interactable {
|
|||||||
private TextReference name;
|
private TextReference name;
|
||||||
private TextReference description;
|
private TextReference description;
|
||||||
private int worthGold;
|
private int worthGold;
|
||||||
|
private int damage;
|
||||||
|
private ItemGrip grip;
|
||||||
private ObjectReference modelRef;
|
private ObjectReference modelRef;
|
||||||
private boolean consumable;
|
private boolean consumable;
|
||||||
private List<ConsumableEffect> effects;
|
private List<ConsumableEffect> effects;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import static de.blight.common.model.ItemCategory.QUEST_ITEMS;
|
|||||||
import static de.blight.common.model.ItemCategory.WEAPON;
|
import static de.blight.common.model.ItemCategory.WEAPON;
|
||||||
import static de.blight.common.model.ItemSubCategory.ARMOR;
|
import static de.blight.common.model.ItemSubCategory.ARMOR;
|
||||||
import static de.blight.common.model.ItemSubCategory.AXE;
|
import static de.blight.common.model.ItemSubCategory.AXE;
|
||||||
|
import static de.blight.common.model.ItemSubCategory.BOW;
|
||||||
|
import static de.blight.common.model.ItemSubCategory.CROSSBOW;
|
||||||
|
import static de.blight.common.model.ItemSubCategory.MAGIC_CRYSTAL;
|
||||||
import static de.blight.common.model.ItemSubCategory.FOOD;
|
import static de.blight.common.model.ItemSubCategory.FOOD;
|
||||||
import static de.blight.common.model.ItemSubCategory.HALBERD;
|
import static de.blight.common.model.ItemSubCategory.HALBERD;
|
||||||
import static de.blight.common.model.ItemSubCategory.HELM;
|
import static de.blight.common.model.ItemSubCategory.HELM;
|
||||||
@@ -36,7 +39,7 @@ public class ItemCategoryManager {
|
|||||||
private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
|
private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE));
|
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL));
|
||||||
mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
|
mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
|
||||||
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
|
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
|
||||||
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));
|
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.blight.common.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/** Position und Ausrichtung des Griffes einer Waffe (in Metern / Grad). */
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class ItemGrip {
|
||||||
|
private float posX, posY, posZ;
|
||||||
|
private float rotX, rotY, rotZ;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package de.blight.common.model;
|
|||||||
|
|
||||||
public enum ItemSubCategory {
|
public enum ItemSubCategory {
|
||||||
|
|
||||||
SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, //WEAPONS
|
SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL, //WEAPONS
|
||||||
RING, NECKLACE, ARMOR, HELM, SHIELD, //GEAR
|
RING, NECKLACE, ARMOR, HELM, SHIELD, //GEAR
|
||||||
POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM,
|
POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM,
|
||||||
QUEST_ITEM,
|
QUEST_ITEM,
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ public class MainCharacter extends GameCharacter {
|
|||||||
|
|
||||||
private de.blight.common.model.abilities.Abilities abilities;
|
private de.blight.common.model.abilities.Abilities abilities;
|
||||||
|
|
||||||
|
/** Aktuell ausgerüstete Nahkampfwaffe (SWORD, AXE oder TWO_HANDED_SWORD). */
|
||||||
|
private Item equippedMeleeWeapon;
|
||||||
|
/** Aktuell ausgerüstete Fernkampfwaffe (BOW, CROSSBOW oder MAGIC_CRYSTAL). */
|
||||||
|
private Item equippedRangedWeapon;
|
||||||
|
/** Schild-Slot – nur gültig wenn equippedMeleeWeapon.subCategory == SWORD. */
|
||||||
|
private Item equippedShield;
|
||||||
|
|
||||||
/** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */
|
/** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */
|
||||||
private Set<String> playedMonologueIds = new HashSet<>();
|
private Set<String> playedMonologueIds = new HashSet<>();
|
||||||
|
|
||||||
@@ -73,6 +80,27 @@ public class MainCharacter extends GameCharacter {
|
|||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
private transient Queue<String> pendingDialogNpcIds = new ArrayDeque<>();
|
private transient Queue<String> pendingDialogNpcIds = new ArrayDeque<>();
|
||||||
|
|
||||||
|
public void equipMelee(Item item) {
|
||||||
|
equippedMeleeWeapon = item;
|
||||||
|
if (item == null || item.getSubCategory() != ItemSubCategory.SWORD) {
|
||||||
|
equippedShield = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void equipRanged(Item item) {
|
||||||
|
equippedRangedWeapon = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void equipShield(Item item) {
|
||||||
|
if (equippedMeleeWeapon != null && equippedMeleeWeapon.getSubCategory() == ItemSubCategory.SWORD) {
|
||||||
|
equippedShield = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unequipMelee() { equipMelee(null); }
|
||||||
|
public void unequipRanged() { equippedRangedWeapon = null; }
|
||||||
|
public void unequipShield() { equippedShield = null; }
|
||||||
|
|
||||||
public void handleDialogOption(DialogOption option) {
|
public void handleDialogOption(DialogOption option) {
|
||||||
if (option.getRequiredItem() != null) {
|
if (option.getRequiredItem() != null) {
|
||||||
getInventar().remove(option.getRequiredItem());
|
getInventar().remove(option.getRequiredItem());
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ public class EditorApp extends Application {
|
|||||||
colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; });
|
colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; });
|
||||||
terrainController.setGrassController(grassController);
|
terrainController.setGrassController(grassController);
|
||||||
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
|
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
|
||||||
|
contentController.setOnEnterContentMode(() -> {
|
||||||
|
if (assetController.isOverlayOpen()) assetController.closeOverlay();
|
||||||
|
assetController.hideTabBtn();
|
||||||
|
});
|
||||||
tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx);
|
tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx);
|
||||||
gameController = new de.blight.editor.toolset.game.GameController(toolCtx);
|
gameController = new de.blight.editor.toolset.game.GameController(toolCtx);
|
||||||
worldPanelController = new de.blight.editor.toolset.world.WorldPanelController(toolCtx, this::scheduleAutoSave);
|
worldPanelController = new de.blight.editor.toolset.world.WorldPanelController(toolCtx, this::scheduleAutoSave);
|
||||||
|
|||||||
@@ -1167,6 +1167,18 @@ public class SharedInput {
|
|||||||
/** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */
|
/** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */
|
||||||
public volatile boolean modelInteractablePosSetFromJme = false;
|
public volatile boolean modelInteractablePosSetFromJme = false;
|
||||||
|
|
||||||
|
// ── Griff-Editor (Waffen-Attachment-Point) ────────────────────────────────
|
||||||
|
/** true → ModelEditorState zeigt den Grip-Gizmo statt des Interactable-Pfeils. */
|
||||||
|
public volatile boolean gripEditorActive = false;
|
||||||
|
public volatile float gripEditorPosX = 0f;
|
||||||
|
public volatile float gripEditorPosY = 0f;
|
||||||
|
public volatile float gripEditorPosZ = 0f;
|
||||||
|
public volatile float gripEditorRotX = 0f;
|
||||||
|
public volatile float gripEditorRotY = 0f;
|
||||||
|
public volatile float gripEditorRotZ = 0f;
|
||||||
|
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
|
||||||
|
public volatile boolean gripEditorGizmoDirty = false;
|
||||||
|
|
||||||
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
|
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
|
||||||
public record SculptEdit(float screenX, float screenY, int action) {}
|
public record SculptEdit(float screenX, float screenY, int action) {}
|
||||||
public final ConcurrentLinkedQueue<SculptEdit> sculptEditQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<SculptEdit> sculptEditQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
private Node interactableArrowNode = null;
|
private Node interactableArrowNode = null;
|
||||||
private float interactableOffsetX = 0f;
|
private float interactableOffsetX = 0f;
|
||||||
private float interactableOffsetY = 0.5f;
|
private float interactableOffsetY = 0.5f;
|
||||||
|
|
||||||
|
/** Grip-Gizmo: 15cm-Pfeil für Waffen-Attachment-Point. */
|
||||||
|
private Node gripGizmoNode = null;
|
||||||
|
private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f;
|
||||||
|
private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f;
|
||||||
private float interactableOffsetZ = 0f;
|
private float interactableOffsetZ = 0f;
|
||||||
private float interactableRotY = 0f;
|
private float interactableRotY = 0f;
|
||||||
|
|
||||||
@@ -232,6 +237,18 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
loadModel(openPath);
|
loadModel(openPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grip-Gizmo aktualisieren
|
||||||
|
if (input.gripEditorActive && input.gripEditorGizmoDirty) {
|
||||||
|
input.gripEditorGizmoDirty = false;
|
||||||
|
gripPosX = input.gripEditorPosX;
|
||||||
|
gripPosY = input.gripEditorPosY;
|
||||||
|
gripPosZ = input.gripEditorPosZ;
|
||||||
|
gripRotX = input.gripEditorRotX;
|
||||||
|
gripRotY = input.gripEditorRotY;
|
||||||
|
gripRotZ = input.gripEditorRotZ;
|
||||||
|
rebuildGripGizmo();
|
||||||
|
}
|
||||||
|
|
||||||
int removeLod = input.modelEditorRemoveEmbeddedLod;
|
int removeLod = input.modelEditorRemoveEmbeddedLod;
|
||||||
if (removeLod > 0) {
|
if (removeLod > 0) {
|
||||||
input.modelEditorRemoveEmbeddedLod = -1;
|
input.modelEditorRemoveEmbeddedLod = -1;
|
||||||
@@ -705,6 +722,17 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
interactableOffsetZ = input.modelInteractableOffsetZ;
|
interactableOffsetZ = input.modelInteractableOffsetZ;
|
||||||
interactableRotY = input.modelInteractableRotY;
|
interactableRotY = input.modelInteractableRotY;
|
||||||
|
|
||||||
|
gripGizmoNode = new Node("gripGizmo");
|
||||||
|
gripGizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||||
|
previewRoot.attachChild(gripGizmoNode);
|
||||||
|
|
||||||
|
gripPosX = input.gripEditorPosX;
|
||||||
|
gripPosY = input.gripEditorPosY;
|
||||||
|
gripPosZ = input.gripEditorPosZ;
|
||||||
|
gripRotX = input.gripEditorRotX;
|
||||||
|
gripRotY = input.gripEditorRotY;
|
||||||
|
gripRotZ = input.gripEditorRotZ;
|
||||||
|
|
||||||
orbitYaw = 30f;
|
orbitYaw = 30f;
|
||||||
orbitPitch = 25f;
|
orbitPitch = 25f;
|
||||||
}
|
}
|
||||||
@@ -726,6 +754,8 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
embeddedLodSpatials = null;
|
embeddedLodSpatials = null;
|
||||||
originalSpatial = null;
|
originalSpatial = null;
|
||||||
interactableArrowNode = null;
|
interactableArrowNode = null;
|
||||||
|
gripGizmoNode = null;
|
||||||
|
input.gripEditorActive = false;
|
||||||
camLight = null;
|
camLight = null;
|
||||||
mainLight = null;
|
mainLight = null;
|
||||||
treeLightMaterials.clear();
|
treeLightMaterials.clear();
|
||||||
@@ -1490,6 +1520,70 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Zeichnet / aktualisiert den 15cm-Grip-Gizmo (orangefarbener Pfeil). */
|
||||||
|
public void rebuildGripGizmo() {
|
||||||
|
if (gripGizmoNode == null) return;
|
||||||
|
gripGizmoNode.detachAllChildren();
|
||||||
|
|
||||||
|
float shaftLen = 0.12f;
|
||||||
|
float shaftRad = 0.008f;
|
||||||
|
float headRad = 0.022f;
|
||||||
|
float headLen = 0.03f; // 15cm gesamt: 12cm Schaft + 3cm Kopf
|
||||||
|
|
||||||
|
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(1f, 0.55f, 0f, 1f));
|
||||||
|
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
matHead.setColor("Color", new ColorRGBA(1f, 0.3f, 0f, 1f));
|
||||||
|
|
||||||
|
Node group = new Node("gripGroup");
|
||||||
|
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
|
||||||
|
|
||||||
|
Quaternion rot = new Quaternion();
|
||||||
|
rot.fromAngles(
|
||||||
|
com.jme3.math.FastMath.DEG_TO_RAD * gripRotX,
|
||||||
|
com.jme3.math.FastMath.DEG_TO_RAD * gripRotY,
|
||||||
|
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
|
||||||
|
group.setLocalRotation(rot);
|
||||||
|
|
||||||
|
// Kleiner Würfel am Griffpunkt
|
||||||
|
Geometry marker = new Geometry("gMarker", new Box(0.015f, 0.015f, 0.015f));
|
||||||
|
marker.setMaterial(mat);
|
||||||
|
|
||||||
|
// Schaft entlang +Z
|
||||||
|
Geometry shaft = new Geometry("gShaft", new Cylinder(4, 8, shaftRad, shaftLen, true));
|
||||||
|
shaft.setMaterial(mat);
|
||||||
|
shaft.setLocalTranslation(0f, 0f, shaftLen * 0.5f);
|
||||||
|
|
||||||
|
// Kegelspitze: Dome zeigt per Default nach +Y → 90° um X → zeigt nach +Z
|
||||||
|
Geometry head = new Geometry("gHead", new Dome(Vector3f.ZERO, 2, 8, headRad, false));
|
||||||
|
head.setMaterial(matHead);
|
||||||
|
head.setLocalTranslation(0f, 0f, shaftLen);
|
||||||
|
Quaternion headRot = new Quaternion();
|
||||||
|
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||||
|
head.setLocalRotation(headRot);
|
||||||
|
|
||||||
|
// Klingenflosse: dünnes flaches Rechteck senkrecht zum Schaft entlang +Y,
|
||||||
|
// damit Rotation um Z (Roll) sichtbar ist. Breite 6cm, Dicke 2mm, entlang Schaft.
|
||||||
|
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
|
||||||
|
Geometry fin = new Geometry("gFin", new Box(0.001f, 0.03f, shaftLen * 0.5f));
|
||||||
|
fin.setMaterial(matFin);
|
||||||
|
fin.setLocalTranslation(0f, 0.03f, shaftLen * 0.5f);
|
||||||
|
|
||||||
|
group.attachChild(marker);
|
||||||
|
group.attachChild(shaft);
|
||||||
|
group.attachChild(fin);
|
||||||
|
group.attachChild(head);
|
||||||
|
gripGizmoNode.attachChild(group);
|
||||||
|
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blendet den Grip-Gizmo ein oder aus. */
|
||||||
|
public void setGripGizmoVisible(boolean visible) {
|
||||||
|
if (gripGizmoNode != null)
|
||||||
|
gripGizmoNode.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
|
||||||
/** Setzt den Pfeil sichtbar/unsichtbar. */
|
/** Setzt den Pfeil sichtbar/unsichtbar. */
|
||||||
public void setInteractableArrowVisible(boolean visible) {
|
public void setInteractableArrowVisible(boolean visible) {
|
||||||
if (interactableArrowNode != null)
|
if (interactableArrowNode != null)
|
||||||
|
|||||||
@@ -84,12 +84,18 @@ public class ContentController {
|
|||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Runnable onEnterContentMode;
|
||||||
|
private GripEditorController gripEditorController;
|
||||||
|
|
||||||
public ContentController(ToolContext ctx) {
|
public ContentController(ToolContext ctx) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
|
this.gripEditorController = new GripEditorController(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Öffentliche API ────────────────────────────────────────────────────────
|
// ── Öffentliche API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void setOnEnterContentMode(Runnable r) { this.onEnterContentMode = r; }
|
||||||
|
|
||||||
public void setDualWindowMenuItem(CheckMenuItem item) {
|
public void setDualWindowMenuItem(CheckMenuItem item) {
|
||||||
this.dualWindowMenuItem = item;
|
this.dualWindowMenuItem = item;
|
||||||
}
|
}
|
||||||
@@ -206,6 +212,7 @@ public class ContentController {
|
|||||||
|
|
||||||
/** 1-Fenster-Content-Modus aktivieren. */
|
/** 1-Fenster-Content-Modus aktivieren. */
|
||||||
public void switchToContentEditor() {
|
public void switchToContentEditor() {
|
||||||
|
if (onEnterContentMode != null) onEnterContentMode.run();
|
||||||
ctx.setCurrentTool().accept("content");
|
ctx.setCurrentTool().accept("content");
|
||||||
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true);
|
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true);
|
||||||
ctx.setMainToolbar().accept(buildSingleWindowContentToolBar());
|
ctx.setMainToolbar().accept(buildSingleWindowContentToolBar());
|
||||||
@@ -328,6 +335,7 @@ public class ContentController {
|
|||||||
if (contentItemBtn != null) contentItemBtn.setSelected(true);
|
if (contentItemBtn != null) contentItemBtn.setSelected(true);
|
||||||
de.blight.editor.ui.ItemEditorView view =
|
de.blight.editor.ui.ItemEditorView view =
|
||||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||||
|
wireItemView(view);
|
||||||
setContentCenter(view);
|
setContentCenter(view);
|
||||||
setContentRight(null);
|
setContentRight(null);
|
||||||
if (selectItemId != null) view.selectItem(selectItemId);
|
if (selectItemId != null) view.selectItem(selectItemId);
|
||||||
@@ -338,6 +346,7 @@ public class ContentController {
|
|||||||
ctx.setMainToolbar().accept(tb);
|
ctx.setMainToolbar().accept(tb);
|
||||||
de.blight.editor.ui.ItemEditorView view =
|
de.blight.editor.ui.ItemEditorView view =
|
||||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||||
|
wireItemView(view);
|
||||||
ctx.setCenterView().accept(view);
|
ctx.setCenterView().accept(view);
|
||||||
ctx.setRightPanel().accept(null);
|
ctx.setRightPanel().accept(null);
|
||||||
if (selectItemId != null) view.selectItem(selectItemId);
|
if (selectItemId != null) view.selectItem(selectItemId);
|
||||||
@@ -949,9 +958,26 @@ public class ContentController {
|
|||||||
if (contentFractionNode != null) contentFractionNode.setExpanded(true);
|
if (contentFractionNode != null) contentFractionNode.setExpanded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void wireItemView(de.blight.editor.ui.ItemEditorView view) {
|
||||||
|
view.setOnIdChanged(this::refreshContentItemTree);
|
||||||
|
view.setGripEditorLauncher((modelPath, currentGrip, onSave) -> {
|
||||||
|
String itemId = view.getCurrentItemId();
|
||||||
|
gripEditorController.setOnReturn(() -> {
|
||||||
|
if (dualWindowMode) {
|
||||||
|
buildItemViewInContent();
|
||||||
|
if (activeItemView != null) activeItemView.selectItem(itemId);
|
||||||
|
} else {
|
||||||
|
switchToItemEditor(itemId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gripEditorController.open(modelPath, currentGrip, onSave);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void buildItemViewInContent() {
|
private void buildItemViewInContent() {
|
||||||
collapseAllContentSections();
|
collapseAllContentSections();
|
||||||
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||||
|
wireItemView(activeItemView);
|
||||||
setContentCenter(activeItemView);
|
setContentCenter(activeItemView);
|
||||||
setContentRight(null);
|
setContentRight(null);
|
||||||
setContentBottom(buildItemLegend());
|
setContentBottom(buildItemLegend());
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package de.blight.editor.toolset.content;
|
||||||
|
|
||||||
|
import de.blight.common.model.ItemGrip;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
import de.blight.editor.ToolContext;
|
||||||
|
import de.blight.editor.state.ModelEditorState;
|
||||||
|
import de.blight.editor.ui.ItemEditorView;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Orientation;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet den Modell-Editor-Viewport mit einem 15cm-Griff-Gizmo (orangefarbener Pfeil).
|
||||||
|
* Position und Rotation des Griffes werden über 6 Spinner eingestellt.
|
||||||
|
*/
|
||||||
|
public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||||
|
|
||||||
|
private final ToolContext ctx;
|
||||||
|
|
||||||
|
private Spinner<Double> posXSpin, posYSpin, posZSpin;
|
||||||
|
private Spinner<Double> rotXSpin, rotYSpin, rotZSpin;
|
||||||
|
|
||||||
|
private Consumer<ItemGrip> pendingOnSave;
|
||||||
|
private Runnable onReturn;
|
||||||
|
private boolean suppressSpinnerEvents = false;
|
||||||
|
|
||||||
|
public void setOnReturn(Runnable r) { this.onReturn = r; }
|
||||||
|
|
||||||
|
public GripEditorController(ToolContext ctx) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GripEditorLauncher ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open(String modelPath, ItemGrip currentGrip, Consumer<ItemGrip> onSave) {
|
||||||
|
pendingOnSave = onSave;
|
||||||
|
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
input.gripEditorActive = true;
|
||||||
|
|
||||||
|
// Grip-Werte aus Item übernehmen (oder Defaults)
|
||||||
|
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
|
||||||
|
input.gripEditorPosX = g.getPosX();
|
||||||
|
input.gripEditorPosY = g.getPosY();
|
||||||
|
input.gripEditorPosZ = g.getPosZ();
|
||||||
|
input.gripEditorRotX = g.getRotX();
|
||||||
|
input.gripEditorRotY = g.getRotY();
|
||||||
|
input.gripEditorRotZ = g.getRotZ();
|
||||||
|
input.gripEditorGizmoDirty = true;
|
||||||
|
|
||||||
|
// Waffenmodell im Modell-Editor laden
|
||||||
|
if (modelPath != null && !modelPath.isBlank()) {
|
||||||
|
input.modelEditorOpenPath = modelPath;
|
||||||
|
}
|
||||||
|
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
|
||||||
|
|
||||||
|
// Hauptfenster: JME-Viewport + Grip-Panel
|
||||||
|
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||||
|
ctx.setCurrentTool().accept("gripEditor");
|
||||||
|
ctx.setMainToolbar().accept(buildToolbar());
|
||||||
|
ctx.setRightPanel().accept(buildPanel(g));
|
||||||
|
ctx.setLeftPanel().accept(null);
|
||||||
|
ctx.setBottomPanel().accept(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toolbar ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ToolBar buildToolbar() {
|
||||||
|
Button cancelBtn = new Button("← Zurück");
|
||||||
|
cancelBtn.setOnAction(e -> onCancel());
|
||||||
|
Label title = new Label("Griff-Editor");
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
ToolBar tb = new ToolBar();
|
||||||
|
tb.getItems().addAll(cancelBtn, new Separator(Orientation.VERTICAL), title);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Panel ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private VBox buildPanel(ItemGrip g) {
|
||||||
|
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
|
||||||
|
posYSpin = doubleSpin(g.getPosY(), -2.0, 2.0, 0.01, "m");
|
||||||
|
posZSpin = doubleSpin(g.getPosZ(), -2.0, 2.0, 0.01, "m");
|
||||||
|
rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°");
|
||||||
|
rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°");
|
||||||
|
rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°");
|
||||||
|
|
||||||
|
posXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||||
|
posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||||
|
posZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||||
|
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||||
|
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||||
|
rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
|
||||||
|
|
||||||
|
Button saveBtn = new Button("Speichern");
|
||||||
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;");
|
||||||
|
saveBtn.setOnAction(e -> onSave());
|
||||||
|
|
||||||
|
Button cancelBtn = new Button("Abbrechen");
|
||||||
|
cancelBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
|
||||||
|
cancelBtn.setOnAction(e -> onCancel());
|
||||||
|
|
||||||
|
VBox panel = new VBox(8);
|
||||||
|
panel.setPadding(new Insets(12));
|
||||||
|
panel.setStyle("-fx-background-color: #252535;");
|
||||||
|
panel.setPrefWidth(220);
|
||||||
|
|
||||||
|
panel.getChildren().addAll(
|
||||||
|
styledTitle("Position (m)"),
|
||||||
|
row("X:", posXSpin),
|
||||||
|
row("Y:", posYSpin),
|
||||||
|
row("Z:", posZSpin),
|
||||||
|
new Separator(),
|
||||||
|
styledTitle("Rotation (°)"),
|
||||||
|
row("X:", rotXSpin),
|
||||||
|
row("Y:", rotYSpin),
|
||||||
|
row("Z:", rotZSpin),
|
||||||
|
new Separator(),
|
||||||
|
styledHint("Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
|
||||||
|
new Separator(),
|
||||||
|
saveBtn,
|
||||||
|
cancelBtn
|
||||||
|
);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Spinner-Events ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onSpinnerChanged() {
|
||||||
|
if (suppressSpinnerEvents) return;
|
||||||
|
SharedInput input = ctx.input();
|
||||||
|
input.gripEditorPosX = posXSpin.getValue().floatValue();
|
||||||
|
input.gripEditorPosY = posYSpin.getValue().floatValue();
|
||||||
|
input.gripEditorPosZ = posZSpin.getValue().floatValue();
|
||||||
|
input.gripEditorRotX = rotXSpin.getValue().floatValue();
|
||||||
|
input.gripEditorRotY = rotYSpin.getValue().floatValue();
|
||||||
|
input.gripEditorRotZ = rotZSpin.getValue().floatValue();
|
||||||
|
input.gripEditorGizmoDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save / Cancel ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onSave() {
|
||||||
|
ItemGrip grip = new ItemGrip();
|
||||||
|
grip.setPosX(posXSpin.getValue().floatValue());
|
||||||
|
grip.setPosY(posYSpin.getValue().floatValue());
|
||||||
|
grip.setPosZ(posZSpin.getValue().floatValue());
|
||||||
|
grip.setRotX(rotXSpin.getValue().floatValue());
|
||||||
|
grip.setRotY(rotYSpin.getValue().floatValue());
|
||||||
|
grip.setRotZ(rotZSpin.getValue().floatValue());
|
||||||
|
|
||||||
|
ctx.input().gripEditorActive = false;
|
||||||
|
|
||||||
|
Consumer<ItemGrip> save = pendingOnSave;
|
||||||
|
Runnable ret = onReturn;
|
||||||
|
pendingOnSave = null;
|
||||||
|
onReturn = null;
|
||||||
|
|
||||||
|
if (save != null) save.accept(grip);
|
||||||
|
if (ret != null) ret.run(); else ctx.switchToWorld().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancel() {
|
||||||
|
ctx.input().gripEditorActive = false;
|
||||||
|
|
||||||
|
Runnable ret = onReturn;
|
||||||
|
pendingOnSave = null;
|
||||||
|
onReturn = null;
|
||||||
|
|
||||||
|
if (ret != null) ret.run(); else ctx.switchToWorld().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static Spinner<Double> doubleSpin(float initial, double min, double max, double step, String unit) {
|
||||||
|
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
|
||||||
|
new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step);
|
||||||
|
vf.setConverter(new javafx.util.StringConverter<>() {
|
||||||
|
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v) + " " + unit; }
|
||||||
|
@Override public Double fromString(String s) {
|
||||||
|
try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); }
|
||||||
|
catch (NumberFormatException e) { return vf.getValue(); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Spinner<Double> sp = new Spinner<>(vf);
|
||||||
|
sp.setEditable(true);
|
||||||
|
sp.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label styledTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #88aacc;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label styledHint(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setWrapText(true);
|
||||||
|
l.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HBox row(String labelText, Spinner<Double> spinner) {
|
||||||
|
Label lbl = new Label(labelText);
|
||||||
|
lbl.setMinWidth(20);
|
||||||
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
|
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||||
|
HBox box = new HBox(6, lbl, spinner);
|
||||||
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import de.blight.common.model.CharacterStat;
|
|||||||
import de.blight.common.model.ConsumableEffect;
|
import de.blight.common.model.ConsumableEffect;
|
||||||
import de.blight.common.model.Item;
|
import de.blight.common.model.Item;
|
||||||
import de.blight.common.model.ItemCategory;
|
import de.blight.common.model.ItemCategory;
|
||||||
|
import de.blight.common.model.ItemGrip;
|
||||||
import de.blight.common.model.ItemCategoryManager;
|
import de.blight.common.model.ItemCategoryManager;
|
||||||
import de.blight.common.model.ItemIO;
|
import de.blight.common.model.ItemIO;
|
||||||
import de.blight.common.model.ItemSubCategory;
|
import de.blight.common.model.ItemSubCategory;
|
||||||
@@ -42,15 +43,24 @@ public class ItemEditorView extends BorderPane {
|
|||||||
private TextField idField;
|
private TextField idField;
|
||||||
private ComboBox<ItemCategory> catCombo;
|
private ComboBox<ItemCategory> catCombo;
|
||||||
private ComboBox<ItemSubCategory> subCatCombo;
|
private ComboBox<ItemSubCategory> subCatCombo;
|
||||||
private TextField nameField;
|
private Label nameLabel;
|
||||||
private TextField descField;
|
private Label descLabel;
|
||||||
private Spinner<Integer> goldSpinner;
|
private Spinner<Integer> goldSpinner;
|
||||||
|
private Spinner<Integer> damageSpinner;
|
||||||
|
private VBox weaponSection;
|
||||||
private TextField modelRefField;
|
private TextField modelRefField;
|
||||||
private VBox formContainer;
|
private VBox formContainer;
|
||||||
private CheckBox consumableCheck;
|
private CheckBox consumableCheck;
|
||||||
private VBox consumablesSection;
|
private VBox consumablesSection;
|
||||||
private VBox effectsRows;
|
private VBox effectsRows;
|
||||||
|
|
||||||
|
public interface GripEditorLauncher {
|
||||||
|
void open(String modelPath, ItemGrip currentGrip, java.util.function.Consumer<ItemGrip> onSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Runnable onIdChanged;
|
||||||
|
private GripEditorLauncher gripEditorLauncher;
|
||||||
|
|
||||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -59,7 +69,7 @@ public class ItemEditorView extends BorderPane {
|
|||||||
|
|
||||||
public ItemEditorView(Path itemDir) {
|
public ItemEditorView(Path itemDir) {
|
||||||
this.itemDir = itemDir;
|
this.itemDir = itemDir;
|
||||||
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot
|
this.assetRoot = itemDir.getParent();
|
||||||
setStyle("-fx-background-color: #1e1e2e;");
|
setStyle("-fx-background-color: #1e1e2e;");
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
@@ -77,6 +87,10 @@ public class ItemEditorView extends BorderPane {
|
|||||||
|
|
||||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void setOnIdChanged(Runnable r) { this.onIdChanged = r; }
|
||||||
|
public void setGripEditorLauncher(GripEditorLauncher l) { this.gripEditorLauncher = l; }
|
||||||
|
public String getCurrentItemId() { return current != null ? current.getItemId() : null; }
|
||||||
|
|
||||||
public void selectItem(String itemId) {
|
public void selectItem(String itemId) {
|
||||||
if (itemId == null) return;
|
if (itemId == null) return;
|
||||||
items.stream()
|
items.stream()
|
||||||
@@ -129,21 +143,6 @@ public class ItemEditorView extends BorderPane {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Legend ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private HBox buildLegend() {
|
|
||||||
HBox box = new HBox(8);
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
for (ItemCategory cat : ItemCategory.values()) {
|
|
||||||
Label dot = new Label("■");
|
|
||||||
dot.setStyle("-fx-text-fill: " + categoryColor(cat) + "; -fx-font-size: 10;");
|
|
||||||
Label lbl = new Label(cat.name());
|
|
||||||
lbl.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
|
|
||||||
box.getChildren().addAll(dot, lbl);
|
|
||||||
}
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form construction ─────────────────────────────────────────────────────
|
// ── Form construction ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
private VBox buildForm() {
|
private VBox buildForm() {
|
||||||
@@ -196,7 +195,6 @@ public class ItemEditorView extends BorderPane {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wenn Kategorie wechselt: SubKategorie-Liste aktualisieren
|
|
||||||
catCombo.valueProperty().addListener((obs, oldCat, newCat) -> {
|
catCombo.valueProperty().addListener((obs, oldCat, newCat) -> {
|
||||||
ItemSubCategory prevSub = subCatCombo.getValue();
|
ItemSubCategory prevSub = subCatCombo.getValue();
|
||||||
subCatCombo.getItems().setAll(
|
subCatCombo.getItems().setAll(
|
||||||
@@ -207,21 +205,47 @@ public class ItemEditorView extends BorderPane {
|
|||||||
} else {
|
} else {
|
||||||
subCatCombo.setValue(null);
|
subCatCombo.setValue(null);
|
||||||
}
|
}
|
||||||
|
updateWeaponSectionVisibility();
|
||||||
|
scheduleSave();
|
||||||
|
});
|
||||||
|
subCatCombo.valueProperty().addListener((obs, o, n) -> {
|
||||||
|
updateWeaponSectionVisibility();
|
||||||
scheduleSave();
|
scheduleSave();
|
||||||
});
|
});
|
||||||
subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
|
||||||
|
|
||||||
nameField = new TextField();
|
// Name und Beschreibung: read-only (auto-generierte TextReference-Schlüssel)
|
||||||
nameField.setPromptText("TextReference-Schlüssel");
|
nameLabel = new Label();
|
||||||
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
nameLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;");
|
||||||
descField = new TextField();
|
nameLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
descField.setPromptText("TextReference-Schlüssel");
|
descLabel = new Label();
|
||||||
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
descLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;");
|
||||||
|
descLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
goldSpinner = new Spinner<>(0, 999999, 0);
|
goldSpinner = new Spinner<>(0, 999999, 0);
|
||||||
goldSpinner.setEditable(true);
|
goldSpinner.setEditable(true);
|
||||||
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||||
|
|
||||||
|
// ── Waffen-Sektion ────────────────────────────────────────────────────
|
||||||
|
damageSpinner = new Spinner<>(0, 99999, 0);
|
||||||
|
damageSpinner.setEditable(true);
|
||||||
|
damageSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
damageSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||||
|
|
||||||
|
Button gripBtn = new Button("Griff definieren...");
|
||||||
|
gripBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
gripBtn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddddff; -fx-cursor: hand;");
|
||||||
|
gripBtn.setOnAction(e -> openGripEditor());
|
||||||
|
|
||||||
|
weaponSection = new VBox(6,
|
||||||
|
new Separator(),
|
||||||
|
sectionTitle("Kampfwerte"),
|
||||||
|
row("Schaden:", damageSpinner),
|
||||||
|
gripBtn
|
||||||
|
);
|
||||||
|
weaponSection.setVisible(false);
|
||||||
|
weaponSection.setManaged(false);
|
||||||
|
|
||||||
modelRefField = new TextField();
|
modelRefField = new TextField();
|
||||||
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||||
modelRefField.setEditable(false);
|
modelRefField.setEditable(false);
|
||||||
@@ -231,10 +255,6 @@ public class ItemEditorView extends BorderPane {
|
|||||||
pickModelBtn.setTooltip(new Tooltip("Modell auswählen"));
|
pickModelBtn.setTooltip(new Tooltip("Modell auswählen"));
|
||||||
pickModelBtn.setOnAction(e -> pickModel());
|
pickModelBtn.setOnAction(e -> pickModel());
|
||||||
|
|
||||||
HBox modelRow = new HBox(4, modelRefField, pickModelBtn);
|
|
||||||
HBox.setHgrow(modelRefField, Priority.ALWAYS);
|
|
||||||
modelRow.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
|
|
||||||
Button clearModelBtn = new Button("✕");
|
Button clearModelBtn = new Button("✕");
|
||||||
clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||||
clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen"));
|
clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen"));
|
||||||
@@ -273,12 +293,13 @@ public class ItemEditorView extends BorderPane {
|
|||||||
row("SubKategorie:", subCatCombo),
|
row("SubKategorie:", subCatCombo),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
sectionTitle("Texte"),
|
sectionTitle("Texte"),
|
||||||
row("Name:", nameField),
|
row("Name:", nameLabel),
|
||||||
row("Beschreibung:", descField),
|
row("Beschreibung:", descLabel),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
sectionTitle("Werte"),
|
sectionTitle("Werte"),
|
||||||
row("Wert (Gold):", goldSpinner),
|
row("Wert (Gold):", goldSpinner),
|
||||||
row("Modell:", modelFullRow),
|
row("Modell:", modelFullRow),
|
||||||
|
weaponSection,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
consumableCheck,
|
consumableCheck,
|
||||||
consumablesSection
|
consumablesSection
|
||||||
@@ -286,6 +307,24 @@ public class ItemEditorView extends BorderPane {
|
|||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openGripEditor() {
|
||||||
|
if (current == null || gripEditorLauncher == null) return;
|
||||||
|
String modelPath = modelRefField.getText().trim();
|
||||||
|
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), grip -> {
|
||||||
|
current.setGrip(grip);
|
||||||
|
persistCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWeaponSectionVisibility() {
|
||||||
|
ItemCategory cat = catCombo.getValue();
|
||||||
|
ItemSubCategory sub = subCatCombo.getValue();
|
||||||
|
boolean isWeapon = cat == ItemCategory.WEAPON
|
||||||
|
&& sub != ItemSubCategory.SHIELD;
|
||||||
|
weaponSection.setVisible(isWeapon);
|
||||||
|
weaponSection.setManaged(isWeapon);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Modell-Auswahl ────────────────────────────────────────────────────────
|
// ── Modell-Auswahl ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void pickModel() {
|
private void pickModel() {
|
||||||
@@ -296,8 +335,10 @@ public class ItemEditorView extends BorderPane {
|
|||||||
private void onIdCommitted() {
|
private void onIdCommitted() {
|
||||||
String id = idField.getText().trim();
|
String id = idField.getText().trim();
|
||||||
if (id.isBlank()) return;
|
if (id.isBlank()) return;
|
||||||
if (nameField.getText().isBlank()) nameField.setText(id + ".name");
|
nameLabel.setText(id + ".name");
|
||||||
if (descField.getText().isBlank()) descField.setText(id + ".description");
|
descLabel.setText(id + ".description");
|
||||||
|
scheduleSave();
|
||||||
|
if (onIdChanged != null) onIdChanged.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||||
@@ -338,11 +379,13 @@ public class ItemEditorView extends BorderPane {
|
|||||||
|
|
||||||
private void loadFormFromItem(Item item) {
|
private void loadFormFromItem(Item item) {
|
||||||
idField.setText(safe(item.getItemId()));
|
idField.setText(safe(item.getItemId()));
|
||||||
catCombo.setValue(item.getCategory()); // löst Listener aus → subCatCombo befüllt
|
catCombo.setValue(item.getCategory());
|
||||||
subCatCombo.setValue(item.getSubCategory());
|
subCatCombo.setValue(item.getSubCategory());
|
||||||
nameField.setText(item.getName() != null ? item.getName().id() : "");
|
String id = safe(item.getItemId());
|
||||||
descField.setText(item.getDescription() != null ? item.getDescription().id() : "");
|
nameLabel.setText(item.getName() != null ? item.getName().id() : (id.isBlank() ? "" : id + ".name"));
|
||||||
|
descLabel.setText(item.getDescription() != null ? item.getDescription().id() : (id.isBlank() ? "" : id + ".description"));
|
||||||
goldSpinner.getValueFactory().setValue(item.getWorthGold());
|
goldSpinner.getValueFactory().setValue(item.getWorthGold());
|
||||||
|
damageSpinner.getValueFactory().setValue(item.getDamage());
|
||||||
ObjectReference ref = item.getModelRef();
|
ObjectReference ref = item.getModelRef();
|
||||||
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
|
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
|
||||||
consumableCheck.setSelected(item.isConsumable());
|
consumableCheck.setSelected(item.isConsumable());
|
||||||
@@ -352,15 +395,19 @@ public class ItemEditorView extends BorderPane {
|
|||||||
addEffectRow(effect.getStat(), effect.getValue());
|
addEffectRow(effect.getStat(), effect.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateWeaponSectionVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveFormToItem(Item item) {
|
private void saveFormToItem(Item item) {
|
||||||
item.setItemId(idField.getText().trim());
|
item.setItemId(idField.getText().trim());
|
||||||
item.setCategory(catCombo.getValue());
|
item.setCategory(catCombo.getValue());
|
||||||
item.setSubCategory(subCatCombo.getValue());
|
item.setSubCategory(subCatCombo.getValue());
|
||||||
item.setName(ref(nameField));
|
String nameKey = nameLabel.getText().trim();
|
||||||
item.setDescription(ref(descField));
|
String descKey = descLabel.getText().trim();
|
||||||
|
item.setName(nameKey.isBlank() ? null : new TextReference(nameKey));
|
||||||
|
item.setDescription(descKey.isBlank() ? null : new TextReference(descKey));
|
||||||
item.setWorthGold(goldSpinner.getValue());
|
item.setWorthGold(goldSpinner.getValue());
|
||||||
|
item.setDamage(damageSpinner.getValue());
|
||||||
String mr = modelRefField.getText().trim();
|
String mr = modelRefField.getText().trim();
|
||||||
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
||||||
item.setConsumable(consumableCheck.isSelected());
|
item.setConsumable(consumableCheck.isSelected());
|
||||||
@@ -378,14 +425,16 @@ public class ItemEditorView extends BorderPane {
|
|||||||
|
|
||||||
private void clearForm() {
|
private void clearForm() {
|
||||||
idField.clear();
|
idField.clear();
|
||||||
catCombo.setValue(null); // setzt subCatCombo via Listener zurück
|
catCombo.setValue(null);
|
||||||
subCatCombo.setValue(null);
|
subCatCombo.setValue(null);
|
||||||
nameField.clear();
|
nameLabel.setText("");
|
||||||
descField.clear();
|
descLabel.setText("");
|
||||||
goldSpinner.getValueFactory().setValue(0);
|
goldSpinner.getValueFactory().setValue(0);
|
||||||
|
damageSpinner.getValueFactory().setValue(0);
|
||||||
modelRefField.clear();
|
modelRefField.clear();
|
||||||
consumableCheck.setSelected(false);
|
consumableCheck.setSelected(false);
|
||||||
effectsRows.getChildren().clear();
|
effectsRows.getChildren().clear();
|
||||||
|
updateWeaponSectionVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── List operations ───────────────────────────────────────────────────────
|
// ── List operations ───────────────────────────────────────────────────────
|
||||||
@@ -453,9 +502,4 @@ public class ItemEditorView extends BorderPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String safe(String s) { return s != null ? s : ""; }
|
private static String safe(String s) { return s != null ? s : ""; }
|
||||||
|
|
||||||
private static TextReference ref(TextField f) {
|
|
||||||
String s = f.getText().trim();
|
|
||||||
return s.isBlank() ? null : new TextReference(s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,37 @@ public enum AnimationAction {
|
|||||||
DISAPPOINT,
|
DISAPPOINT,
|
||||||
SECRET,
|
SECRET,
|
||||||
TALK_SITTING,
|
TALK_SITTING,
|
||||||
SWORD_ATTACK_1;
|
|
||||||
|
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||||
|
SWORD_ATTACK_1,
|
||||||
|
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
|
||||||
|
SWORD_ATTACK_2, SWORD_HEAVY_ATTACK,
|
||||||
|
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
||||||
|
|
||||||
|
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||||
|
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
|
||||||
|
AXE_ATTACK_1, AXE_ATTACK_2, AXE_HEAVY_ATTACK,
|
||||||
|
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||||
|
|
||||||
|
// ── Zweihänder ────────────────────────────────────────────────────────────
|
||||||
|
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
|
||||||
|
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_HEAVY_ATTACK,
|
||||||
|
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||||
|
|
||||||
|
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||||
|
BOW_DRAW, BOW_SHEATHE, BOW_HIT_TAKEN,
|
||||||
|
BOW_AIM, BOW_FIRE, BOW_RELOAD,
|
||||||
|
BOW_IDLE, BOW_WALK, BOW_RUN,
|
||||||
|
|
||||||
|
// ── Armbrust ──────────────────────────────────────────────────────────────
|
||||||
|
CROSSBOW_DRAW, CROSSBOW_SHEATHE, CROSSBOW_HIT_TAKEN,
|
||||||
|
CROSSBOW_AIM, CROSSBOW_FIRE, CROSSBOW_RELOAD,
|
||||||
|
CROSSBOW_IDLE, CROSSBOW_WALK, CROSSBOW_RUN,
|
||||||
|
|
||||||
|
// ── Magiekristall ─────────────────────────────────────────────────────────
|
||||||
|
CRYSTAL_DRAW, CRYSTAL_SHEATHE, CRYSTAL_HIT_TAKEN,
|
||||||
|
CRYSTAL_AIM, CRYSTAL_FIRE, CRYSTAL_RELOAD,
|
||||||
|
CRYSTAL_IDLE, CRYSTAL_WALK, CRYSTAL_RUN;
|
||||||
|
|
||||||
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
|
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
|
||||||
public String displayName() {
|
public String displayName() {
|
||||||
@@ -61,7 +91,69 @@ public enum AnimationAction {
|
|||||||
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
|
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
|
||||||
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
||||||
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
|
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
|
||||||
case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1(Schwert)");
|
|
||||||
|
case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Schwert)");
|
||||||
|
case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)");
|
||||||
|
case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)");
|
||||||
|
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
|
||||||
|
case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||||
|
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
||||||
|
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||||
|
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||||
|
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
|
||||||
|
case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)");
|
||||||
|
|
||||||
|
case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)");
|
||||||
|
case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)");
|
||||||
|
case AXE_BLOCK -> TextRegistry.resolve(null, key, "Block (Axt)");
|
||||||
|
case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||||
|
case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
|
||||||
|
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
|
||||||
|
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||||
|
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||||
|
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
|
||||||
|
case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)");
|
||||||
|
|
||||||
|
case TWO_HAND_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Zweihänder)");
|
||||||
|
case TWO_HAND_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Zweihänder)");
|
||||||
|
case TWO_HAND_BLOCK -> TextRegistry.resolve(null, key, "Block (Zweihänder)");
|
||||||
|
case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||||
|
case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)");
|
||||||
|
case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
|
||||||
|
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||||
|
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||||
|
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
|
||||||
|
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");
|
||||||
|
|
||||||
|
case BOW_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Bogen)");
|
||||||
|
case BOW_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Bogen)");
|
||||||
|
case BOW_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Bogen)");
|
||||||
|
case BOW_AIM -> TextRegistry.resolve(null, key, "Zielen (Bogen)");
|
||||||
|
case BOW_FIRE -> TextRegistry.resolve(null, key, "Abfeuern (Bogen)");
|
||||||
|
case BOW_RELOAD -> TextRegistry.resolve(null, key, "Nachladen (Bogen)");
|
||||||
|
case BOW_IDLE -> TextRegistry.resolve(null, key, "Stehen (Bogen)");
|
||||||
|
case BOW_WALK -> TextRegistry.resolve(null, key, "Gehen (Bogen)");
|
||||||
|
case BOW_RUN -> TextRegistry.resolve(null, key, "Laufen (Bogen)");
|
||||||
|
|
||||||
|
case CROSSBOW_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Armbrust)");
|
||||||
|
case CROSSBOW_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Armbrust)");
|
||||||
|
case CROSSBOW_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Armbrust)");
|
||||||
|
case CROSSBOW_AIM -> TextRegistry.resolve(null, key, "Zielen (Armbrust)");
|
||||||
|
case CROSSBOW_FIRE -> TextRegistry.resolve(null, key, "Abfeuern (Armbrust)");
|
||||||
|
case CROSSBOW_RELOAD -> TextRegistry.resolve(null, key, "Nachladen (Armbrust)");
|
||||||
|
case CROSSBOW_IDLE -> TextRegistry.resolve(null, key, "Stehen (Armbrust)");
|
||||||
|
case CROSSBOW_WALK -> TextRegistry.resolve(null, key, "Gehen (Armbrust)");
|
||||||
|
case CROSSBOW_RUN -> TextRegistry.resolve(null, key, "Laufen (Armbrust)");
|
||||||
|
|
||||||
|
case CRYSTAL_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Kristall)");
|
||||||
|
case CRYSTAL_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Kristall)");
|
||||||
|
case CRYSTAL_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Kristall)");
|
||||||
|
case CRYSTAL_AIM -> TextRegistry.resolve(null, key, "Zielen (Kristall)");
|
||||||
|
case CRYSTAL_FIRE -> TextRegistry.resolve(null, key, "Abfeuern (Kristall)");
|
||||||
|
case CRYSTAL_RELOAD -> TextRegistry.resolve(null, key, "Nachladen (Kristall)");
|
||||||
|
case CRYSTAL_IDLE -> TextRegistry.resolve(null, key, "Stehen (Kristall)");
|
||||||
|
case CRYSTAL_WALK -> TextRegistry.resolve(null, key, "Gehen (Kristall)");
|
||||||
|
case CRYSTAL_RUN -> TextRegistry.resolve(null, key, "Laufen (Kristall)");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public class KeyBindings {
|
|||||||
public int quicksave = KeyInput.KEY_F5;
|
public int quicksave = KeyInput.KEY_F5;
|
||||||
public int character = KeyInput.KEY_C;
|
public int character = KeyInput.KEY_C;
|
||||||
public int quests = KeyInput.KEY_V;
|
public int quests = KeyInput.KEY_V;
|
||||||
|
public int drawMeleeWeapon = KeyInput.KEY_1;
|
||||||
|
public int drawRangedWeapon = KeyInput.KEY_2;
|
||||||
|
public int sheathWeapon = KeyInput.KEY_X;
|
||||||
|
|
||||||
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
||||||
public static final String[][] ENTRIES = {
|
public static final String[][] ENTRIES = {
|
||||||
@@ -36,6 +39,9 @@ public class KeyBindings {
|
|||||||
{"quicksave", "Schnellspeichern"},
|
{"quicksave", "Schnellspeichern"},
|
||||||
{"character", "Charakter"},
|
{"character", "Charakter"},
|
||||||
{"quests", "Questlog"},
|
{"quests", "Questlog"},
|
||||||
|
{"drawMeleeWeapon", "Nahkampfwaffe ziehen"},
|
||||||
|
{"drawRangedWeapon", "Fernkampfwaffe ziehen"},
|
||||||
|
{"sheathWeapon", "Waffe wegstecken"},
|
||||||
};
|
};
|
||||||
|
|
||||||
public int get(String fieldName) {
|
public int get(String fieldName) {
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package de.blight.game.control;
|
|||||||
import com.jme3.anim.AnimComposer;
|
import com.jme3.anim.AnimComposer;
|
||||||
import com.jme3.bullet.control.CharacterControl;
|
import com.jme3.bullet.control.CharacterControl;
|
||||||
import com.jme3.input.InputManager;
|
import com.jme3.input.InputManager;
|
||||||
|
import com.jme3.input.MouseInput;
|
||||||
import com.jme3.input.controls.ActionListener;
|
import com.jme3.input.controls.ActionListener;
|
||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
|
import de.blight.common.model.ItemSubCategory;
|
||||||
|
import de.blight.common.model.MainCharacter;
|
||||||
import com.jme3.math.Quaternion;
|
import com.jme3.math.Quaternion;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
import com.jme3.renderer.Camera;
|
import com.jme3.renderer.Camera;
|
||||||
@@ -35,8 +39,16 @@ public class PlayerInputControl {
|
|||||||
private static final float WALK_MULT = 0.5f;
|
private static final float WALK_MULT = 0.5f;
|
||||||
private static final float ROTATE_SPEED = 10f;
|
private static final float ROTATE_SPEED = 10f;
|
||||||
|
|
||||||
private static final String[] ACTION_NAMES =
|
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
|
||||||
{"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk"};
|
private static final String ACT_DRAW_RANGED = "DrawRangedWeapon";
|
||||||
|
private static final String ACT_SHEATH = "SheathWeapon";
|
||||||
|
private static final String ACT_COMBAT_ATK = "CombatAttack";
|
||||||
|
private static final String ACT_COMBAT_ALT = "CombatAltAction";
|
||||||
|
|
||||||
|
private static final String[] ACTION_NAMES = {
|
||||||
|
"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk",
|
||||||
|
ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT
|
||||||
|
};
|
||||||
|
|
||||||
private final InputManager inputManager;
|
private final InputManager inputManager;
|
||||||
private final Camera cam;
|
private final Camera cam;
|
||||||
@@ -47,6 +59,17 @@ public class PlayerInputControl {
|
|||||||
private boolean forward, backward, left, right, sprint, walk;
|
private boolean forward, backward, left, right, sprint, walk;
|
||||||
private boolean paused = false;
|
private boolean paused = false;
|
||||||
|
|
||||||
|
// ── Kampf-Zustand ─────────────────────────────────────────────────────────
|
||||||
|
private MainCharacter mainCharacter = null;
|
||||||
|
private boolean weaponDrawn = false;
|
||||||
|
private ItemSubCategory drawnWeaponType = null;
|
||||||
|
private boolean isAiming = false;
|
||||||
|
private boolean meleeAttackToggle = false;
|
||||||
|
private boolean rightMouseHeld = false;
|
||||||
|
private boolean combatAnimActive = false;
|
||||||
|
private float combatAnimRemaining = 0f;
|
||||||
|
private Runnable combatAnimCallback = null;
|
||||||
|
|
||||||
private AnimationLibrary animLib;
|
private AnimationLibrary animLib;
|
||||||
private String animSetName;
|
private String animSetName;
|
||||||
private Path assetRoot;
|
private Path assetRoot;
|
||||||
@@ -139,6 +162,11 @@ public class PlayerInputControl {
|
|||||||
case "Sprint" -> sprint = isPressed;
|
case "Sprint" -> sprint = isPressed;
|
||||||
case "Walk" -> walk = isPressed;
|
case "Walk" -> walk = isPressed;
|
||||||
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } }
|
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } }
|
||||||
|
case ACT_DRAW_MELEE -> { if (isPressed) handleDrawMelee(); }
|
||||||
|
case ACT_DRAW_RANGED -> { if (isPressed) handleDrawRanged(); }
|
||||||
|
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive) sheathCurrentWeapon(); }
|
||||||
|
case ACT_COMBAT_ATK -> { if (isPressed) handleCombatAttack(); }
|
||||||
|
case ACT_COMBAT_ALT -> handleCombatAlt(isPressed);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,6 +238,11 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||||
|
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||||
|
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||||
|
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||||
|
|
||||||
public boolean isPaused() { return paused; }
|
public boolean isPaused() { return paused; }
|
||||||
|
|
||||||
public void setPaused(boolean paused) {
|
public void setPaused(boolean paused) {
|
||||||
@@ -219,6 +252,9 @@ public class PlayerInputControl {
|
|||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
stopWading();
|
stopWading();
|
||||||
|
rightMouseHeld = false;
|
||||||
|
combatAnimActive = false;
|
||||||
|
combatAnimCallback = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,6 +506,11 @@ public class PlayerInputControl {
|
|||||||
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
|
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
|
||||||
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
|
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
|
||||||
inputManager.addMapping("Walk", new KeyTrigger(kb.walk));
|
inputManager.addMapping("Walk", new KeyTrigger(kb.walk));
|
||||||
|
inputManager.addMapping(ACT_DRAW_MELEE, new KeyTrigger(kb.drawMeleeWeapon));
|
||||||
|
inputManager.addMapping(ACT_DRAW_RANGED, new KeyTrigger(kb.drawRangedWeapon));
|
||||||
|
inputManager.addMapping(ACT_SHEATH, new KeyTrigger(kb.sheathWeapon));
|
||||||
|
inputManager.addMapping(ACT_COMBAT_ATK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
|
inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
|
||||||
inputManager.addListener(actionListener, ACTION_NAMES);
|
inputManager.addListener(actionListener, ACTION_NAMES);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +640,7 @@ public class PlayerInputControl {
|
|||||||
if (moving) {
|
if (moving) {
|
||||||
scratchMoveDir.normalizeLocal();
|
scratchMoveDir.normalizeLocal();
|
||||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||||
: sprint ? MOVE_SPEED * SPRINT_MULT
|
: (sprint && !weaponDrawn) ? MOVE_SPEED * SPRINT_MULT
|
||||||
: MOVE_SPEED;
|
: MOVE_SPEED;
|
||||||
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
|
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
|
||||||
|
|
||||||
@@ -613,10 +654,23 @@ public class PlayerInputControl {
|
|||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kampf-Anim-Timer
|
||||||
|
if (combatAnimActive) {
|
||||||
|
combatAnimRemaining -= tpf;
|
||||||
|
if (combatAnimRemaining <= 0f) {
|
||||||
|
combatAnimActive = false;
|
||||||
|
currentAnim = null;
|
||||||
|
Runnable cb = combatAnimCallback;
|
||||||
|
combatAnimCallback = null;
|
||||||
|
if (cb != null) cb.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Animations-Auswahl
|
// Animations-Auswahl
|
||||||
if (jumpFrames > 0) jumpFrames--;
|
if (jumpFrames > 0) jumpFrames--;
|
||||||
if (groundGraceFrames > 0) groundGraceFrames--;
|
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||||
|
|
||||||
|
if (!combatAnimActive) {
|
||||||
AnimationAction target;
|
AnimationAction target;
|
||||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||||
@@ -628,10 +682,17 @@ public class PlayerInputControl {
|
|||||||
target = AnimationAction.IDLE;
|
target = AnimationAction.IDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target = resolveWeaponAction(target);
|
||||||
|
|
||||||
|
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
|
||||||
|
target = getMeleeBlockAction(drawnWeaponType);
|
||||||
|
}
|
||||||
|
|
||||||
if (target != currentAnim) {
|
if (target != currentAnim) {
|
||||||
playAction(target);
|
playAction(target);
|
||||||
currentAnim = target;
|
currentAnim = target;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pollFootsteps(tpf);
|
pollFootsteps(tpf);
|
||||||
}
|
}
|
||||||
@@ -654,6 +715,244 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Kampf-Hilfsmethoden ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleDrawMelee() {
|
||||||
|
if (mainCharacter == null || mainCharacter.getEquippedMeleeWeapon() == null) return;
|
||||||
|
if (blockingAnimActive || combatAnimActive) return;
|
||||||
|
ItemSubCategory sub = mainCharacter.getEquippedMeleeWeapon().getSubCategory();
|
||||||
|
if (!weaponDrawn) {
|
||||||
|
AnimationAction drawAnim = getMeleeDrawAction(sub);
|
||||||
|
float dur = resolveClipLength(drawAnim, 0.5f);
|
||||||
|
requestAnimation(drawAnim, dur, () -> {
|
||||||
|
weaponDrawn = true;
|
||||||
|
drawnWeaponType = sub;
|
||||||
|
currentAnim = null;
|
||||||
|
});
|
||||||
|
} else if (isMeleeType(drawnWeaponType)) {
|
||||||
|
sheathCurrentWeapon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDrawRanged() {
|
||||||
|
if (mainCharacter == null || mainCharacter.getEquippedRangedWeapon() == null) return;
|
||||||
|
if (blockingAnimActive || combatAnimActive) return;
|
||||||
|
ItemSubCategory sub = mainCharacter.getEquippedRangedWeapon().getSubCategory();
|
||||||
|
if (!weaponDrawn) {
|
||||||
|
AnimationAction drawAnim = getRangedDrawAction(sub);
|
||||||
|
float dur = resolveClipLength(drawAnim, 0.5f);
|
||||||
|
requestAnimation(drawAnim, dur, () -> {
|
||||||
|
weaponDrawn = true;
|
||||||
|
drawnWeaponType = sub;
|
||||||
|
currentAnim = null;
|
||||||
|
});
|
||||||
|
} else if (isRangedType(drawnWeaponType)) {
|
||||||
|
sheathCurrentWeapon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sheathCurrentWeapon() {
|
||||||
|
if (drawnWeaponType == null) { weaponDrawn = false; return; }
|
||||||
|
isAiming = false;
|
||||||
|
rightMouseHeld = false;
|
||||||
|
AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
|
||||||
|
float dur = resolveClipLength(sheathAnim, 0.5f);
|
||||||
|
requestAnimation(sheathAnim, dur, () -> {
|
||||||
|
weaponDrawn = false;
|
||||||
|
drawnWeaponType = null;
|
||||||
|
currentAnim = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCombatAttack() {
|
||||||
|
if (!weaponDrawn || blockingAnimActive || combatAnimActive) return;
|
||||||
|
if (isMeleeType(drawnWeaponType)) {
|
||||||
|
AnimationAction atk = meleeAttackToggle
|
||||||
|
? getMeleeAttack2Action(drawnWeaponType)
|
||||||
|
: getMeleeAttack1Action(drawnWeaponType);
|
||||||
|
meleeAttackToggle = !meleeAttackToggle;
|
||||||
|
float dur = resolveClipLength(atk, 0.6f);
|
||||||
|
combatAnimActive = true;
|
||||||
|
// Blend-out zu Idle startet BLEND_DURATION früher → kein sichtbares Einfrieren am letzten Frame
|
||||||
|
combatAnimRemaining = Math.max(0.1f, dur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||||
|
combatAnimCallback = null;
|
||||||
|
playAction(atk);
|
||||||
|
currentAnim = atk;
|
||||||
|
} else if (isRangedType(drawnWeaponType) && isAiming) {
|
||||||
|
isAiming = false;
|
||||||
|
AnimationAction fireAnim = getRangedFireAction(drawnWeaponType);
|
||||||
|
AnimationAction reloadAnim = getRangedReloadAction(drawnWeaponType);
|
||||||
|
float fireDur = resolveClipLength(fireAnim, 0.5f);
|
||||||
|
float reloadDur = resolveClipLength(reloadAnim, 0.8f);
|
||||||
|
combatAnimActive = true;
|
||||||
|
combatAnimRemaining = Math.max(0.1f, fireDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||||
|
combatAnimCallback = () -> {
|
||||||
|
combatAnimActive = true;
|
||||||
|
combatAnimRemaining = Math.max(0.1f, reloadDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||||
|
combatAnimCallback = null;
|
||||||
|
playAction(reloadAnim);
|
||||||
|
currentAnim = reloadAnim;
|
||||||
|
};
|
||||||
|
playAction(fireAnim);
|
||||||
|
currentAnim = fireAnim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCombatAlt(boolean isPressed) {
|
||||||
|
if (!weaponDrawn || blockingAnimActive) return;
|
||||||
|
if (isMeleeType(drawnWeaponType)) {
|
||||||
|
rightMouseHeld = isPressed;
|
||||||
|
} else if (isRangedType(drawnWeaponType) && isPressed) {
|
||||||
|
isAiming = !isAiming;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationAction resolveWeaponAction(AnimationAction base) {
|
||||||
|
if (!weaponDrawn || drawnWeaponType == null) return base;
|
||||||
|
return switch (base) {
|
||||||
|
case IDLE -> weaponIdleAction();
|
||||||
|
case WALK -> weaponWalkAction();
|
||||||
|
case RUN, SPRINT -> weaponRunAction();
|
||||||
|
default -> base;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationAction weaponIdleAction() {
|
||||||
|
if (isAiming) {
|
||||||
|
return switch (drawnWeaponType) {
|
||||||
|
case BOW -> AnimationAction.BOW_AIM;
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_AIM;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_AIM;
|
||||||
|
default -> weaponStandAction();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return weaponStandAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationAction weaponStandAction() {
|
||||||
|
return switch (drawnWeaponType) {
|
||||||
|
case SWORD -> AnimationAction.SWORD_IDLE;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE;
|
||||||
|
case AXE -> AnimationAction.AXE_IDLE;
|
||||||
|
case BOW -> AnimationAction.BOW_IDLE;
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_IDLE;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_IDLE;
|
||||||
|
default -> AnimationAction.IDLE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationAction weaponWalkAction() {
|
||||||
|
return switch (drawnWeaponType) {
|
||||||
|
case SWORD -> AnimationAction.SWORD_WALK;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_WALK;
|
||||||
|
case AXE -> AnimationAction.AXE_WALK;
|
||||||
|
case BOW -> AnimationAction.BOW_WALK;
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_WALK;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_WALK;
|
||||||
|
default -> AnimationAction.WALK;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnimationAction weaponRunAction() {
|
||||||
|
return switch (drawnWeaponType) {
|
||||||
|
case SWORD -> AnimationAction.SWORD_RUN;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_RUN;
|
||||||
|
case AXE -> AnimationAction.AXE_RUN;
|
||||||
|
case BOW -> AnimationAction.BOW_RUN;
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_RUN;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_RUN;
|
||||||
|
default -> AnimationAction.RUN;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMeleeType(ItemSubCategory s) {
|
||||||
|
return s == ItemSubCategory.SWORD
|
||||||
|
|| s == ItemSubCategory.AXE
|
||||||
|
|| s == ItemSubCategory.TWO_HANDED_SWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRangedType(ItemSubCategory s) {
|
||||||
|
return s == ItemSubCategory.BOW
|
||||||
|
|| s == ItemSubCategory.CROSSBOW
|
||||||
|
|| s == ItemSubCategory.MAGIC_CRYSTAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getMeleeDrawAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AXE -> AnimationAction.AXE_DRAW;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_DRAW;
|
||||||
|
default -> AnimationAction.SWORD_DRAW;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getMeleySheathAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AXE -> AnimationAction.AXE_SHEATHE;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE;
|
||||||
|
default -> AnimationAction.SWORD_SHEATHE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getMeleeAttack1Action(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AXE -> AnimationAction.AXE_ATTACK_1;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_1;
|
||||||
|
default -> AnimationAction.SWORD_ATTACK_1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getMeleeAttack2Action(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AXE -> AnimationAction.AXE_ATTACK_2;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_2;
|
||||||
|
default -> AnimationAction.SWORD_ATTACK_2;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getMeleeBlockAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AXE -> AnimationAction.AXE_BLOCK;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK;
|
||||||
|
default -> AnimationAction.SWORD_BLOCK;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_DRAW;
|
||||||
|
default -> AnimationAction.BOW_DRAW;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getRangedSheathAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_SHEATHE;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_SHEATHE;
|
||||||
|
default -> AnimationAction.BOW_SHEATHE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getRangedFireAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_FIRE;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_FIRE;
|
||||||
|
default -> AnimationAction.BOW_FIRE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getRangedReloadAction(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case CROSSBOW -> AnimationAction.CROSSBOW_RELOAD;
|
||||||
|
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_RELOAD;
|
||||||
|
default -> AnimationAction.BOW_RELOAD;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getSheathAction(ItemSubCategory s) {
|
||||||
|
return isMeleeType(s) ? getMeleySheathAction(s) : getRangedSheathAction(s);
|
||||||
|
}
|
||||||
|
|
||||||
public void setFootstepSystem(de.blight.game.audio.FootstepSystem fs,
|
public void setFootstepSystem(de.blight.game.audio.FootstepSystem fs,
|
||||||
java.util.function.BiFunction<Float, Float, float[]> query) {
|
java.util.function.BiFunction<Float, Float, float[]> query) {
|
||||||
this.footstepSystem = fs;
|
this.footstepSystem = fs;
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
package de.blight.game.control;
|
||||||
|
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.math.Matrix4f;
|
||||||
|
import com.jme3.math.Quaternion;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.renderer.RenderManager;
|
||||||
|
import com.jme3.renderer.ViewPort;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.scene.control.AbstractControl;
|
||||||
|
import de.blight.common.model.Item;
|
||||||
|
import de.blight.common.model.ItemGrip;
|
||||||
|
import de.blight.common.model.MainCharacter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hängt die ausgerüstete Waffe an den Charakter:
|
||||||
|
* - weggesteckt → Spine2-Knochen (Rücken, Klinge zeigt nach unten)
|
||||||
|
* - gezückt → RightHand-Knochen (unter Berücksichtigung des gespeicherten ItemGrip)
|
||||||
|
*
|
||||||
|
* Die Waffe wird direkt am character-Node (kein geerbter Scale) als Kind eingehängt
|
||||||
|
* und bekommt jeden Frame die korrekte Position/Rotation aus dem Bone-World-Transform.
|
||||||
|
*/
|
||||||
|
public class WeaponAttachmentControl extends AbstractControl {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentControl.class);
|
||||||
|
|
||||||
|
private static final String BONE_SPINE = "mixamorig:Spine2";
|
||||||
|
private static final String BONE_HAND = "mixamorig:RightHand";
|
||||||
|
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||||||
|
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||||||
|
|
||||||
|
// Grip-Zielposition im Spine2-lokalen Raum (Meter): Griff ~40cm über dem Brustbein
|
||||||
|
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.05f, 0.40f, -0.08f);
|
||||||
|
|
||||||
|
private final AssetManager assetManager;
|
||||||
|
private final PlayerInputControl playerInput;
|
||||||
|
private final MainCharacter mc;
|
||||||
|
|
||||||
|
private com.jme3.anim.SkinningControl skinCtrl;
|
||||||
|
private Node backNode; // Spine2-Attachment-Node (im skalierten Char-Modell)
|
||||||
|
private Node handNode; // RightHand-Attachment-Node
|
||||||
|
|
||||||
|
// Bind-Pose-Rotation des RightHand-Knochens in Character-lokalem Raum (T-Pose).
|
||||||
|
// Wird einmalig beim Initialisieren aus dem InverseBindMatrix abgeleitet.
|
||||||
|
// Erlaubt, die Grip-Rotation aus dem Modell-Editor direkt im Spiel zu verwenden.
|
||||||
|
private Quaternion handBindRotInChar;
|
||||||
|
|
||||||
|
private Item lastEquippedWeapon;
|
||||||
|
private boolean lastDrawnState = false;
|
||||||
|
private Spatial weaponSpatial;
|
||||||
|
|
||||||
|
public WeaponAttachmentControl(AssetManager assetManager,
|
||||||
|
PlayerInputControl playerInput,
|
||||||
|
MainCharacter mc) {
|
||||||
|
this.assetManager = assetManager;
|
||||||
|
this.playerInput = playerInput;
|
||||||
|
this.mc = mc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void controlUpdate(float tpf) {
|
||||||
|
// Lazy init: SkinningControl erst nach setAnimationContext() verfügbar
|
||||||
|
if (skinCtrl == null) {
|
||||||
|
skinCtrl = playerInput.getSkinningControl();
|
||||||
|
if (skinCtrl == null) return;
|
||||||
|
try {
|
||||||
|
backNode = skinCtrl.getAttachmentsNode(BONE_SPINE);
|
||||||
|
log.info("[WeaponAttach] backNode ({}): {}", BONE_SPINE, backNode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handNode = skinCtrl.getAttachmentsNode(BONE_HAND);
|
||||||
|
log.info("[WeaponAttach] handNode ({}): {}", BONE_HAND, handNode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage());
|
||||||
|
}
|
||||||
|
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
|
||||||
|
log.info("[WeaponAttach] handBindRotInChar: {}", handBindRotInChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Item current = mc.getEquippedMeleeWeapon();
|
||||||
|
boolean drawn = playerInput.isWeaponDrawn();
|
||||||
|
|
||||||
|
if (current != lastEquippedWeapon) {
|
||||||
|
log.info("[WeaponAttach] Waffe geändert: {} → {}",
|
||||||
|
lastEquippedWeapon != null ? lastEquippedWeapon.getItemId() : "null",
|
||||||
|
current != null ? current.getItemId() : "null");
|
||||||
|
detachWeapon();
|
||||||
|
lastEquippedWeapon = current;
|
||||||
|
if (current != null) {
|
||||||
|
loadWeapon(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weaponSpatial != null) {
|
||||||
|
lastDrawnState = drawn;
|
||||||
|
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Laden ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadWeapon(Item item) {
|
||||||
|
if (item.getModelRef() == null || item.getModelRef().getPath() == null) {
|
||||||
|
log.warn("[WeaponAttach] Kein modelRef an Item '{}'.", item.getItemId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
weaponSpatial = assetManager.loadModel(item.getModelRef().getPath());
|
||||||
|
// Wichtig: Waffe direkt am character-Node hängen (ohne geerbten 0.02-Scale
|
||||||
|
// aus der Charakter-Modell-Hierarchie). Position/Rotation wird jedes Frame
|
||||||
|
// aus den Bone-World-Transforms berechnet.
|
||||||
|
((Node) spatial).attachChild(weaponSpatial);
|
||||||
|
log.info("[WeaponAttach] Waffe geladen und an character-Node gehängt: {}",
|
||||||
|
item.getModelRef().getPath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
|
||||||
|
item.getModelRef().getPath(), e.getMessage());
|
||||||
|
weaponSpatial = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Transform-Update (jedes Frame) ────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateWeaponTransform(boolean drawn, ItemGrip grip) {
|
||||||
|
Node boneNode = drawn ? handNode : backNode;
|
||||||
|
if (boneNode == null) return;
|
||||||
|
|
||||||
|
// Bone-World-Transform (korrekt, auch wenn Eltern skaliert sind)
|
||||||
|
Vector3f boneWorldPos = boneNode.getWorldTranslation();
|
||||||
|
Quaternion boneWorldRot = boneNode.getWorldRotation();
|
||||||
|
|
||||||
|
// In den lokalen Raum des character-Nodes transformieren
|
||||||
|
// (character hat in der Regel nur Translation + Rotation, keinen Scale)
|
||||||
|
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
||||||
|
Vector3f boneInChar = charWorldRotInv.mult(
|
||||||
|
boneWorldPos.subtract(spatial.getWorldTranslation()));
|
||||||
|
Quaternion boneRotInChar = charWorldRotInv.mult(boneWorldRot);
|
||||||
|
|
||||||
|
if (drawn) {
|
||||||
|
applyHandTransform(boneInChar, boneRotInChar, grip);
|
||||||
|
} else {
|
||||||
|
applyBackTransform(boneInChar, boneRotInChar, grip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waffe in der Hand: Griff-Punkt soll am Handknochen liegen, Rotation aus ItemGrip.
|
||||||
|
*
|
||||||
|
* Die Grip-Rotation ist im Character-/Welt-Raum definiert (wie im Grip-Editor sichtbar).
|
||||||
|
* Der Animations-Delta (Bewegung relativ zur Bind-Pose) wird zusätzlich aufaddiert,
|
||||||
|
* damit die Waffe den Handknochen durch alle Animationen folgt.
|
||||||
|
*/
|
||||||
|
private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||||
|
Quaternion gripRot = (grip != null)
|
||||||
|
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
|
||||||
|
: new Quaternion();
|
||||||
|
Vector3f gripPos = (grip != null)
|
||||||
|
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||||
|
: Vector3f.ZERO;
|
||||||
|
|
||||||
|
// animDelta = wie weit der Knochen sich von der Bind-Pose bewegt hat.
|
||||||
|
// In der Bind-Pose (T-Pose / Idle) ist animDelta ≈ identity → weaponRot ≈ gripRot.
|
||||||
|
// Während Angriffsanimationen dreht animDelta die Waffe mit der Hand mit.
|
||||||
|
Quaternion animDelta = (handBindRotInChar != null)
|
||||||
|
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||||||
|
: boneRotInChar;
|
||||||
|
|
||||||
|
Quaternion weaponRot = animDelta.mult(gripRot);
|
||||||
|
|
||||||
|
// Waffenposition: Griff-Punkt liegt am Knochen-Ursprung
|
||||||
|
weaponSpatial.setLocalRotation(weaponRot);
|
||||||
|
weaponSpatial.setLocalTranslation(bonePosInChar.subtract(weaponRot.mult(gripPos)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Liest die Bind-Pose-Rotation eines Knochens (Model-Raum ≈ Character-lokal) aus dem InverseBindMatrix. */
|
||||||
|
private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) {
|
||||||
|
com.jme3.anim.Armature arm = sc.getArmature();
|
||||||
|
if (arm == null) return null;
|
||||||
|
com.jme3.anim.Joint joint = arm.getJoint(jointName);
|
||||||
|
if (joint == null) return null;
|
||||||
|
Matrix4f invBind = joint.getInverseModelBindMatrix();
|
||||||
|
if (invBind == null) return null;
|
||||||
|
Matrix4f bindMat = invBind.invert();
|
||||||
|
if (bindMat == null) return null;
|
||||||
|
return bindMat.toRotationQuat().normalizeLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waffe am Rücken: BACK_GRIP_OFFSET über Spine2, Klinge zeigt nach unten.
|
||||||
|
*/
|
||||||
|
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||||
|
// fromAngles(0, 0, HALF_PI): Spine2-FBX-Flip kompensiert, Klinge zeigt nach unten
|
||||||
|
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
||||||
|
Quaternion weaponRot = boneRotInChar.mult(backRot);
|
||||||
|
|
||||||
|
// Zielposition des Griff-Punktes: BACK_GRIP_OFFSET im Knochen-lokalen Raum
|
||||||
|
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||||
|
|
||||||
|
Vector3f gripPos = (grip != null)
|
||||||
|
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||||
|
: Vector3f.ZERO;
|
||||||
|
|
||||||
|
// weaponPos + weaponRot * gripPos = gripTarget
|
||||||
|
weaponSpatial.setLocalRotation(weaponRot);
|
||||||
|
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Detach ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void detachWeapon() {
|
||||||
|
if (weaponSpatial != null) {
|
||||||
|
weaponSpatial.removeFromParent();
|
||||||
|
weaponSpatial = null;
|
||||||
|
}
|
||||||
|
lastDrawnState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||||
|
}
|
||||||
@@ -361,6 +361,8 @@ public class WorldScene extends BaseAppState {
|
|||||||
|
|
||||||
MainCharacter mc = findMainCharacter();
|
MainCharacter mc = findMainCharacter();
|
||||||
if (mc != null) {
|
if (mc != null) {
|
||||||
|
playerInput.setMainCharacter(mc);
|
||||||
|
character.addControl(new de.blight.game.control.WeaponAttachmentControl(assetManager, playerInput, mc));
|
||||||
// SaveGameState mit Charakter und Positions-Lieferant verknüpfen
|
// SaveGameState mit Charakter und Positions-Lieferant verknüpfen
|
||||||
de.blight.game.state.SaveGameState saveState =
|
de.blight.game.state.SaveGameState saveState =
|
||||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import com.jme3.input.controls.ActionListener;
|
|||||||
import com.jme3.input.controls.AnalogListener;
|
import com.jme3.input.controls.AnalogListener;
|
||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
import com.jme3.input.controls.MouseAxisTrigger;
|
import com.jme3.input.controls.MouseAxisTrigger;
|
||||||
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
|
import de.blight.common.model.Item;
|
||||||
|
import de.blight.common.model.ItemSubCategory;
|
||||||
import de.blight.common.model.MainCharacter;
|
import de.blight.common.model.MainCharacter;
|
||||||
import de.blight.common.model.XPHelper;
|
import de.blight.common.model.XPHelper;
|
||||||
import de.blight.common.model.abilities.Abilities;
|
import de.blight.common.model.abilities.Abilities;
|
||||||
@@ -49,6 +52,7 @@ public class CharacterState extends OverlayState {
|
|||||||
private static final String MAP_TOGGLE = "_CharToggle";
|
private static final String MAP_TOGGLE = "_CharToggle";
|
||||||
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
||||||
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
||||||
|
private static final String MAP_EQUIP_CLICK = "_CharEquipClick";
|
||||||
|
|
||||||
// ── Scroll-Zustand ────────────────────────────────────────────────────────
|
// ── Scroll-Zustand ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -64,6 +68,11 @@ public class CharacterState extends OverlayState {
|
|||||||
|
|
||||||
private final MainCharacter mc;
|
private final MainCharacter mc;
|
||||||
|
|
||||||
|
// ── Equipment-Slot-Bounds (für Klick-Erkennung) ───────────────────────────
|
||||||
|
private float[] meleeSlotBounds = null;
|
||||||
|
private float[] shieldSlotBounds = null;
|
||||||
|
private float[] rangedSlotBounds = null;
|
||||||
|
|
||||||
private record Skill(String name, int lvl, int maxLvl, String[][] abils) {}
|
private record Skill(String name, int lvl, int maxLvl, String[][] abils) {}
|
||||||
|
|
||||||
public CharacterState(MainCharacter mc) {
|
public CharacterState(MainCharacter mc) {
|
||||||
@@ -82,6 +91,8 @@ public class CharacterState extends OverlayState {
|
|||||||
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||||
app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||||
app.getInputManager().addListener(scrollAnalog, MAP_SCROLL_UP, MAP_SCROLL_DOWN);
|
app.getInputManager().addListener(scrollAnalog, MAP_SCROLL_UP, MAP_SCROLL_DOWN);
|
||||||
|
app.getInputManager().addMapping(MAP_EQUIP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
|
app.getInputManager().addListener(equipClickListener, MAP_EQUIP_CLICK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -89,13 +100,18 @@ public class CharacterState extends OverlayState {
|
|||||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||||
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
|
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
|
||||||
|
app.getInputManager().deleteMapping(MAP_EQUIP_CLICK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void buildContent(Node panel, float scale) {
|
protected void buildContent(Node panel, float scale) {
|
||||||
|
meleeSlotBounds = null;
|
||||||
|
shieldSlotBounds = null;
|
||||||
|
rangedSlotBounds = null;
|
||||||
float divX = PX + PAD + LEFT_W + 6f;
|
float divX = PX + PAD + LEFT_W + 6f;
|
||||||
addQuad(panel, divX, PY + PAD, 1f, PH - HDR_H - PAD * 2, UiTheme.COL_DIVIDER, -17f);
|
addQuad(panel, divX, PY + PAD, 1f, PH - HDR_H - PAD * 2, UiTheme.COL_DIVIDER, -17f);
|
||||||
buildStats(panel);
|
float equipY = buildStats(panel);
|
||||||
|
buildEquipment(panel, equipY);
|
||||||
buildAbilities(panel);
|
buildAbilities(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +136,22 @@ public class CharacterState extends OverlayState {
|
|||||||
rebuildSkills();
|
rebuildSkills();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private final ActionListener equipClickListener = (name, isPressed, tpf) -> {
|
||||||
|
if (!isPressed || !isEnabled()) return;
|
||||||
|
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||||
|
if (hitTest(v, meleeSlotBounds)) { cycleEquipMelee(); return; }
|
||||||
|
if (hitTest(v, shieldSlotBounds)) { cycleEquipShield(); return; }
|
||||||
|
if (hitTest(v, rangedSlotBounds)) { cycleEquipRanged(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static boolean hitTest(float[] v, float[] b) {
|
||||||
|
if (b == null) return false;
|
||||||
|
return v[0] >= b[0] && v[0] <= b[0] + b[2] && v[1] >= b[1] && v[1] <= b[1] + b[3];
|
||||||
|
}
|
||||||
|
|
||||||
// ── Linke Spalte: Werte ───────────────────────────────────────────────────
|
// ── Linke Spalte: Werte ───────────────────────────────────────────────────
|
||||||
|
|
||||||
private void buildStats(Node panel) {
|
private float buildStats(Node panel) {
|
||||||
float x = PX + PAD;
|
float x = PX + PAD;
|
||||||
float y = PY + PH - HDR_H - PAD;
|
float y = PY + PH - HDR_H - PAD;
|
||||||
|
|
||||||
@@ -172,6 +201,7 @@ public class CharacterState extends OverlayState {
|
|||||||
regenLine(panel, "HP", hpReg, x, y); y -= 16;
|
regenLine(panel, "HP", hpReg, x, y); y -= 16;
|
||||||
regenLine(panel, "Stamina", stReg, x, y); y -= 16;
|
regenLine(panel, "Stamina", stReg, x, y); y -= 16;
|
||||||
regenLine(panel, "Mana", mnReg, x, y);
|
regenLine(panel, "Mana", mnReg, x, y);
|
||||||
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
private float statBar(Node parent, String label, int cur, int max, ColorRGBA col,
|
private float statBar(Node parent, String label, int cur, int max, ColorRGBA col,
|
||||||
@@ -191,6 +221,109 @@ public class CharacterState extends OverlayState {
|
|||||||
return y - BAR_H_STAT - 10;
|
return y - BAR_H_STAT - 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Ausrüstungs-Panel ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void buildEquipment(Node panel, float startY) {
|
||||||
|
float x = PX + PAD;
|
||||||
|
float y = startY - 26f;
|
||||||
|
|
||||||
|
addQuad(panel, x, y, LEFT_W - 6, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||||
|
y -= 14;
|
||||||
|
|
||||||
|
BitmapText lbl = crispText("Ausrüstung", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||||
|
lbl.setLocalTranslation(x, y, -17);
|
||||||
|
panel.attachChild(lbl);
|
||||||
|
y -= 20;
|
||||||
|
|
||||||
|
String meleeName = mc.getEquippedMeleeWeapon() != null
|
||||||
|
? mc.getEquippedMeleeWeapon().getDisplayText() : "—";
|
||||||
|
meleeSlotBounds = equipSlotRow(panel, "Nahkampf", meleeName, x, y, LEFT_W - 6);
|
||||||
|
y -= 26;
|
||||||
|
|
||||||
|
boolean hasSword = mc.getEquippedMeleeWeapon() != null
|
||||||
|
&& mc.getEquippedMeleeWeapon().getSubCategory() == ItemSubCategory.SWORD;
|
||||||
|
if (hasSword) {
|
||||||
|
String shieldName = mc.getEquippedShield() != null
|
||||||
|
? mc.getEquippedShield().getDisplayText() : "—";
|
||||||
|
shieldSlotBounds = equipSlotRow(panel, "Schild", shieldName, x, y, LEFT_W - 6);
|
||||||
|
y -= 26;
|
||||||
|
} else {
|
||||||
|
shieldSlotBounds = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String rangedName = mc.getEquippedRangedWeapon() != null
|
||||||
|
? mc.getEquippedRangedWeapon().getDisplayText() : "—";
|
||||||
|
rangedSlotBounds = equipSlotRow(panel, "Fernkampf", rangedName, x, y, LEFT_W - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] equipSlotRow(Node panel, String label, String value,
|
||||||
|
float x, float y, float w) {
|
||||||
|
float slotH = 22f;
|
||||||
|
addQuad(panel, x, y - slotH, w, slotH, UiTheme.COL_CARD_BRD, -19f);
|
||||||
|
addQuad(panel, x+1, y - slotH + 1, w-2, slotH-2, UiTheme.COL_CARD, -18f);
|
||||||
|
|
||||||
|
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||||
|
lblTxt.setLocalTranslation(x + 4, y - 5, -17f);
|
||||||
|
panel.attachChild(lblTxt);
|
||||||
|
|
||||||
|
String clipped = value.length() > 16 ? value.substring(0, 15) + "…" : value;
|
||||||
|
BitmapText valTxt = crispText(clipped, UiTheme.FONT_SMALL, UiTheme.COL_WHITE);
|
||||||
|
valTxt.setLocalTranslation(x + 76, y - 5, -17f);
|
||||||
|
panel.attachChild(valTxt);
|
||||||
|
|
||||||
|
BitmapText arr = crispText("»", UiTheme.FONT_TINY, UiTheme.COL_MUTED);
|
||||||
|
arr.setLocalTranslation(x + w - 16, y - 5, -17f);
|
||||||
|
panel.attachChild(arr);
|
||||||
|
|
||||||
|
return new float[]{ x, y - slotH, w, slotH };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cycleEquipMelee() {
|
||||||
|
java.util.List<Item> options = new java.util.ArrayList<>();
|
||||||
|
options.add(null);
|
||||||
|
if (mc.getInventar() != null) {
|
||||||
|
mc.getInventar().getItems().keySet().stream()
|
||||||
|
.filter(i -> i.getSubCategory() == ItemSubCategory.SWORD
|
||||||
|
|| i.getSubCategory() == ItemSubCategory.AXE
|
||||||
|
|| i.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD)
|
||||||
|
.sorted(java.util.Comparator.comparing(Item::getDisplayText))
|
||||||
|
.forEach(options::add);
|
||||||
|
}
|
||||||
|
int idx = options.indexOf(mc.getEquippedMeleeWeapon());
|
||||||
|
mc.equipMelee(options.get((idx + 1) % options.size()));
|
||||||
|
setEnabled(false); setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cycleEquipShield() {
|
||||||
|
java.util.List<Item> options = new java.util.ArrayList<>();
|
||||||
|
options.add(null);
|
||||||
|
if (mc.getInventar() != null) {
|
||||||
|
mc.getInventar().getItems().keySet().stream()
|
||||||
|
.filter(i -> i.getSubCategory() == ItemSubCategory.SHIELD)
|
||||||
|
.sorted(java.util.Comparator.comparing(Item::getDisplayText))
|
||||||
|
.forEach(options::add);
|
||||||
|
}
|
||||||
|
int idx = options.indexOf(mc.getEquippedShield());
|
||||||
|
mc.equipShield(options.get((idx + 1) % options.size()));
|
||||||
|
setEnabled(false); setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cycleEquipRanged() {
|
||||||
|
java.util.List<Item> options = new java.util.ArrayList<>();
|
||||||
|
options.add(null);
|
||||||
|
if (mc.getInventar() != null) {
|
||||||
|
mc.getInventar().getItems().keySet().stream()
|
||||||
|
.filter(i -> i.getSubCategory() == ItemSubCategory.BOW
|
||||||
|
|| i.getSubCategory() == ItemSubCategory.CROSSBOW
|
||||||
|
|| i.getSubCategory() == ItemSubCategory.MAGIC_CRYSTAL)
|
||||||
|
.sorted(java.util.Comparator.comparing(Item::getDisplayText))
|
||||||
|
.forEach(options::add);
|
||||||
|
}
|
||||||
|
int idx = options.indexOf(mc.getEquippedRangedWeapon());
|
||||||
|
mc.equipRanged(options.get((idx + 1) % options.size()));
|
||||||
|
setEnabled(false); setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
private void regenLine(Node parent, String label, int val, float x, float y) {
|
private void regenLine(Node parent, String label, int val, float x, float y) {
|
||||||
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
|
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
|
||||||
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||||
|
|||||||
@@ -225,16 +225,43 @@ public class InventoryState extends OverlayState {
|
|||||||
|
|
||||||
// ── Item-Grid ─────────────────────────────────────────────────────────────
|
// ── Item-Grid ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final float WEAPON_SLOT_H = 26f;
|
||||||
|
|
||||||
|
private void buildWeaponSlots(Node parent) {
|
||||||
|
float x = contentLeft;
|
||||||
|
float y = contentTop;
|
||||||
|
float w = COLS * (CELL_W + CELL_GAP) - CELL_GAP;
|
||||||
|
|
||||||
|
String[] labels = {"Slot 1 — Nahkampf", "Slot 2 — Fernkampf"};
|
||||||
|
Item[] items = {mc.getEquippedMeleeWeapon(), mc.getEquippedRangedWeapon()};
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
y -= WEAPON_SLOT_H;
|
||||||
|
addQuad(parent, x, y, w, WEAPON_SLOT_H, UiTheme.COL_SECTION, -18f);
|
||||||
|
String slotTxt = labels[i] + ": " + (items[i] != null ? items[i].getDisplayText() : "—");
|
||||||
|
BitmapText t = crispText(slotTxt, UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||||
|
t.setLocalTranslation(x + 8, y + WEAPON_SLOT_H - 7, -17f);
|
||||||
|
parent.attachChild(t);
|
||||||
|
y -= 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void buildGrid() {
|
private void buildGrid() {
|
||||||
gridNode = new Node("inv-grid");
|
gridNode = new Node("inv-grid");
|
||||||
panel.attachChild(gridNode);
|
panel.attachChild(gridNode);
|
||||||
|
|
||||||
|
float savedContentTop = contentTop;
|
||||||
|
if (activeTab == ItemCategory.WEAPON) {
|
||||||
|
buildWeaponSlots(gridNode);
|
||||||
|
contentTop -= 2 * (WEAPON_SLOT_H + 4);
|
||||||
|
}
|
||||||
|
|
||||||
if (activeItems.isEmpty()) {
|
if (activeItems.isEmpty()) {
|
||||||
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
|
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
|
||||||
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
|
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
|
||||||
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
|
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
|
||||||
empty.setLocalTranslation(ex, ey, -17);
|
empty.setLocalTranslation(ex, ey, -17);
|
||||||
gridNode.attachChild(empty);
|
gridNode.attachChild(empty);
|
||||||
|
contentTop = savedContentTop;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +305,7 @@ public class InventoryState extends OverlayState {
|
|||||||
if (col != 0) { virtY -= CELL_GAP; }
|
if (col != 0) { virtY -= CELL_GAP; }
|
||||||
virtY -= CELL_GAP;
|
virtY -= CELL_GAP;
|
||||||
}
|
}
|
||||||
|
contentTop = savedContentTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rebuildGrid() {
|
private void rebuildGrid() {
|
||||||
@@ -297,7 +325,8 @@ public class InventoryState extends OverlayState {
|
|||||||
int rows = (int) Math.ceil(g.size() / (double) COLS);
|
int rows = (int) Math.ceil(g.size() / (double) COLS);
|
||||||
totalH += rows * (CELL_H + CELL_GAP);
|
totalH += rows * (CELL_H + CELL_GAP);
|
||||||
}
|
}
|
||||||
float contentH = contentTop - contentBottom;
|
float slotOffset = (activeTab == ItemCategory.WEAPON) ? 2 * (WEAPON_SLOT_H + 4) : 0f;
|
||||||
|
float contentH = contentTop - contentBottom - slotOffset;
|
||||||
return Math.max(0f, totalH - contentH);
|
return Math.max(0f, totalH - contentH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +421,9 @@ public class InventoryState extends OverlayState {
|
|||||||
case STAFF -> "Stäbe";
|
case STAFF -> "Stäbe";
|
||||||
case HALBERD -> "Hellebarden";
|
case HALBERD -> "Hellebarden";
|
||||||
case AXE -> "Äxte";
|
case AXE -> "Äxte";
|
||||||
|
case BOW -> "Bögen";
|
||||||
|
case CROSSBOW -> "Armbrüste";
|
||||||
|
case MAGIC_CRYSTAL -> "Magiekristalle";
|
||||||
case RING -> "Ringe";
|
case RING -> "Ringe";
|
||||||
case NECKLACE -> "Halsketten";
|
case NECKLACE -> "Halsketten";
|
||||||
case ARMOR -> "Rüstungen";
|
case ARMOR -> "Rüstungen";
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ public class SaveGameState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ausgerüstete Waffen
|
||||||
|
save.character.equippedMeleeWeaponId = mc.getEquippedMeleeWeapon() != null ? mc.getEquippedMeleeWeapon().getItemId() : null;
|
||||||
|
save.character.equippedRangedWeaponId = mc.getEquippedRangedWeapon() != null ? mc.getEquippedRangedWeapon().getItemId() : null;
|
||||||
|
save.character.equippedShieldId = mc.getEquippedShield() != null ? mc.getEquippedShield().getItemId() : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SaveGameIO.save(save);
|
SaveGameIO.save(save);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
// ── Annäherung ────────────────────────────────────────────────────────────
|
// ── Annäherung ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void startApproach(int idx) {
|
private void startApproach(int idx) {
|
||||||
|
if (playerInput.isWeaponDrawn()) return;
|
||||||
targetIdx = idx;
|
targetIdx = idx;
|
||||||
walkTimer = 0f;
|
walkTimer = 0f;
|
||||||
originPos = physicsChar.getPhysicsLocation().clone();
|
originPos = physicsChar.getPhysicsLocation().clone();
|
||||||
|
|||||||
@@ -173,6 +173,20 @@ public class WorldItemsState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
log.info("[WorldItems] Inventar wiederhergestellt ({} Einträge).", cs.inventory.size());
|
log.info("[WorldItems] Inventar wiederhergestellt ({} Einträge).", cs.inventory.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ausgerüstete Waffen wiederherstellen
|
||||||
|
if (cs.equippedMeleeWeaponId != null) {
|
||||||
|
Item m = itemDefs.get(cs.equippedMeleeWeaponId);
|
||||||
|
if (m != null) mainCharacter.equipMelee(m);
|
||||||
|
}
|
||||||
|
if (cs.equippedRangedWeaponId != null) {
|
||||||
|
Item r = itemDefs.get(cs.equippedRangedWeaponId);
|
||||||
|
if (r != null) mainCharacter.equipRanged(r);
|
||||||
|
}
|
||||||
|
if (cs.equippedShieldId != null) {
|
||||||
|
Item s = itemDefs.get(cs.equippedShieldId);
|
||||||
|
if (s != null) mainCharacter.equipShield(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputManager.addMapping(INTERACT_ACTION,
|
inputManager.addMapping(INTERACT_ACTION,
|
||||||
@@ -421,6 +435,7 @@ public class WorldItemsState extends BaseAppState {
|
|||||||
// ── Pickup-Logik ──────────────────────────────────────────────────────────
|
// ── Pickup-Logik ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void tryPickup() {
|
private void tryPickup() {
|
||||||
|
if (playerInput.isWeaponDrawn()) return;
|
||||||
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
|
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
|
||||||
if (hoveredIdx < 0 || physicsChar == null) return;
|
if (hoveredIdx < 0 || physicsChar == null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ quest.kuehles_bier.success
|
|||||||
quest.magnetiterz.description
|
quest.magnetiterz.description
|
||||||
quest.magnetiterz.name
|
quest.magnetiterz.name
|
||||||
quest.magnetiterz.success
|
quest.magnetiterz.success
|
||||||
|
rostiges_kurzschwert.description
|
||||||
|
rostiges_kurzschwert.name
|
||||||
silas.name
|
silas.name
|
||||||
strandgut.description
|
strandgut.description
|
||||||
strandgut.name
|
strandgut.name
|
||||||
|
|||||||
2
blight-map/src/main/map/blight_placed_items.bpi
Normal file
2
blight-map/src/main/map/blight_placed_items.bpi
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# uuid itemId x y z
|
||||||
|
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693
|
||||||
Reference in New Issue
Block a user