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",
|
||||
"Stand To Sit",
|
||||
"T-Pose",
|
||||
"Walking"
|
||||
"Walking",
|
||||
"Jumping",
|
||||
"Sword And Shield Slash"
|
||||
],
|
||||
"actionMap": {
|
||||
"DEFAULT": "Idle",
|
||||
@@ -17,7 +19,10 @@
|
||||
"SIT_DOWN": "Stand To Sit",
|
||||
"SITTING": "Sitting Idle",
|
||||
"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",
|
||||
"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;
|
||||
/** itemId → Anzahl */
|
||||
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 {
|
||||
|
||||
@@ -15,6 +15,8 @@ public class Item implements Interactable {
|
||||
private TextReference name;
|
||||
private TextReference description;
|
||||
private int worthGold;
|
||||
private int damage;
|
||||
private ItemGrip grip;
|
||||
private ObjectReference modelRef;
|
||||
private boolean consumable;
|
||||
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.ItemSubCategory.ARMOR;
|
||||
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.HALBERD;
|
||||
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);
|
||||
|
||||
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(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_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 {
|
||||
|
||||
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
|
||||
POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM,
|
||||
QUEST_ITEM,
|
||||
|
||||
@@ -44,6 +44,13 @@ public class MainCharacter extends GameCharacter {
|
||||
|
||||
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. */
|
||||
private Set<String> playedMonologueIds = new HashSet<>();
|
||||
|
||||
@@ -73,6 +80,27 @@ public class MainCharacter extends GameCharacter {
|
||||
@Setter(AccessLevel.NONE)
|
||||
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) {
|
||||
if (option.getRequiredItem() != null) {
|
||||
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 : ""; });
|
||||
terrainController.setGrassController(grassController);
|
||||
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);
|
||||
gameController = new de.blight.editor.toolset.game.GameController(toolCtx);
|
||||
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. */
|
||||
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. */
|
||||
public record SculptEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<SculptEdit> sculptEditQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
@@ -99,6 +99,11 @@ public class ModelEditorState extends BaseAppState {
|
||||
private Node interactableArrowNode = null;
|
||||
private float interactableOffsetX = 0f;
|
||||
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 interactableRotY = 0f;
|
||||
|
||||
@@ -232,6 +237,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
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;
|
||||
if (removeLod > 0) {
|
||||
input.modelEditorRemoveEmbeddedLod = -1;
|
||||
@@ -705,6 +722,17 @@ public class ModelEditorState extends BaseAppState {
|
||||
interactableOffsetZ = input.modelInteractableOffsetZ;
|
||||
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;
|
||||
orbitPitch = 25f;
|
||||
}
|
||||
@@ -726,6 +754,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
embeddedLodSpatials = null;
|
||||
originalSpatial = null;
|
||||
interactableArrowNode = null;
|
||||
gripGizmoNode = null;
|
||||
input.gripEditorActive = false;
|
||||
camLight = null;
|
||||
mainLight = null;
|
||||
treeLightMaterials.clear();
|
||||
@@ -1490,6 +1520,70 @@ public class ModelEditorState extends BaseAppState {
|
||||
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. */
|
||||
public void setInteractableArrowVisible(boolean visible) {
|
||||
if (interactableArrowNode != null)
|
||||
|
||||
@@ -84,12 +84,18 @@ public class ContentController {
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private Runnable onEnterContentMode;
|
||||
private GripEditorController gripEditorController;
|
||||
|
||||
public ContentController(ToolContext ctx) {
|
||||
this.ctx = ctx;
|
||||
this.gripEditorController = new GripEditorController(ctx);
|
||||
}
|
||||
|
||||
// ── Öffentliche API ────────────────────────────────────────────────────────
|
||||
|
||||
public void setOnEnterContentMode(Runnable r) { this.onEnterContentMode = r; }
|
||||
|
||||
public void setDualWindowMenuItem(CheckMenuItem item) {
|
||||
this.dualWindowMenuItem = item;
|
||||
}
|
||||
@@ -206,6 +212,7 @@ public class ContentController {
|
||||
|
||||
/** 1-Fenster-Content-Modus aktivieren. */
|
||||
public void switchToContentEditor() {
|
||||
if (onEnterContentMode != null) onEnterContentMode.run();
|
||||
ctx.setCurrentTool().accept("content");
|
||||
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true);
|
||||
ctx.setMainToolbar().accept(buildSingleWindowContentToolBar());
|
||||
@@ -328,6 +335,7 @@ public class ContentController {
|
||||
if (contentItemBtn != null) contentItemBtn.setSelected(true);
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(view);
|
||||
setContentCenter(view);
|
||||
setContentRight(null);
|
||||
if (selectItemId != null) view.selectItem(selectItemId);
|
||||
@@ -338,6 +346,7 @@ public class ContentController {
|
||||
ctx.setMainToolbar().accept(tb);
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(view);
|
||||
ctx.setCenterView().accept(view);
|
||||
ctx.setRightPanel().accept(null);
|
||||
if (selectItemId != null) view.selectItem(selectItemId);
|
||||
@@ -949,9 +958,26 @@ public class ContentController {
|
||||
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() {
|
||||
collapseAllContentSections();
|
||||
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(activeItemView);
|
||||
setContentCenter(activeItemView);
|
||||
setContentRight(null);
|
||||
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.Item;
|
||||
import de.blight.common.model.ItemCategory;
|
||||
import de.blight.common.model.ItemGrip;
|
||||
import de.blight.common.model.ItemCategoryManager;
|
||||
import de.blight.common.model.ItemIO;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
@@ -42,14 +43,23 @@ public class ItemEditorView extends BorderPane {
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private ComboBox<ItemSubCategory> subCatCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private CheckBox consumableCheck;
|
||||
private VBox consumablesSection;
|
||||
private VBox effectsRows;
|
||||
private Label nameLabel;
|
||||
private Label descLabel;
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private Spinner<Integer> damageSpinner;
|
||||
private VBox weaponSection;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private CheckBox consumableCheck;
|
||||
private VBox consumablesSection;
|
||||
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));
|
||||
|
||||
@@ -59,7 +69,7 @@ public class ItemEditorView extends BorderPane {
|
||||
|
||||
public ItemEditorView(Path itemDir) {
|
||||
this.itemDir = itemDir;
|
||||
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot
|
||||
this.assetRoot = itemDir.getParent();
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
|
||||
@@ -77,6 +87,10 @@ public class ItemEditorView extends BorderPane {
|
||||
|
||||
// ── Ö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) {
|
||||
if (itemId == null) return;
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildForm() {
|
||||
@@ -196,7 +195,6 @@ public class ItemEditorView extends BorderPane {
|
||||
}
|
||||
});
|
||||
|
||||
// Wenn Kategorie wechselt: SubKategorie-Liste aktualisieren
|
||||
catCombo.valueProperty().addListener((obs, oldCat, newCat) -> {
|
||||
ItemSubCategory prevSub = subCatCombo.getValue();
|
||||
subCatCombo.getItems().setAll(
|
||||
@@ -207,21 +205,47 @@ public class ItemEditorView extends BorderPane {
|
||||
} else {
|
||||
subCatCombo.setValue(null);
|
||||
}
|
||||
updateWeaponSectionVisibility();
|
||||
scheduleSave();
|
||||
});
|
||||
subCatCombo.valueProperty().addListener((obs, o, n) -> {
|
||||
updateWeaponSectionVisibility();
|
||||
scheduleSave();
|
||||
});
|
||||
subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
// Name und Beschreibung: read-only (auto-generierte TextReference-Schlüssel)
|
||||
nameLabel = new Label();
|
||||
nameLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;");
|
||||
nameLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
descLabel = new Label();
|
||||
descLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;");
|
||||
descLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
goldSpinner = new Spinner<>(0, 999999, 0);
|
||||
goldSpinner.setEditable(true);
|
||||
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
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.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||
modelRefField.setEditable(false);
|
||||
@@ -231,10 +255,6 @@ public class ItemEditorView extends BorderPane {
|
||||
pickModelBtn.setTooltip(new Tooltip("Modell auswählen"));
|
||||
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("✕");
|
||||
clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen"));
|
||||
@@ -273,12 +293,13 @@ public class ItemEditorView extends BorderPane {
|
||||
row("SubKategorie:", subCatCombo),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Name:", nameField),
|
||||
row("Beschreibung:", descField),
|
||||
row("Name:", nameLabel),
|
||||
row("Beschreibung:", descLabel),
|
||||
new Separator(),
|
||||
sectionTitle("Werte"),
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelFullRow),
|
||||
weaponSection,
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection
|
||||
@@ -286,6 +307,24 @@ public class ItemEditorView extends BorderPane {
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
private void pickModel() {
|
||||
@@ -296,8 +335,10 @@ public class ItemEditorView extends BorderPane {
|
||||
private void onIdCommitted() {
|
||||
String id = idField.getText().trim();
|
||||
if (id.isBlank()) return;
|
||||
if (nameField.getText().isBlank()) nameField.setText(id + ".name");
|
||||
if (descField.getText().isBlank()) descField.setText(id + ".description");
|
||||
nameLabel.setText(id + ".name");
|
||||
descLabel.setText(id + ".description");
|
||||
scheduleSave();
|
||||
if (onIdChanged != null) onIdChanged.run();
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
@@ -338,11 +379,13 @@ public class ItemEditorView extends BorderPane {
|
||||
|
||||
private void loadFormFromItem(Item item) {
|
||||
idField.setText(safe(item.getItemId()));
|
||||
catCombo.setValue(item.getCategory()); // löst Listener aus → subCatCombo befüllt
|
||||
catCombo.setValue(item.getCategory());
|
||||
subCatCombo.setValue(item.getSubCategory());
|
||||
nameField.setText(item.getName() != null ? item.getName().id() : "");
|
||||
descField.setText(item.getDescription() != null ? item.getDescription().id() : "");
|
||||
String id = safe(item.getItemId());
|
||||
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());
|
||||
damageSpinner.getValueFactory().setValue(item.getDamage());
|
||||
ObjectReference ref = item.getModelRef();
|
||||
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
|
||||
consumableCheck.setSelected(item.isConsumable());
|
||||
@@ -352,15 +395,19 @@ public class ItemEditorView extends BorderPane {
|
||||
addEffectRow(effect.getStat(), effect.getValue());
|
||||
}
|
||||
}
|
||||
updateWeaponSectionVisibility();
|
||||
}
|
||||
|
||||
private void saveFormToItem(Item item) {
|
||||
item.setItemId(idField.getText().trim());
|
||||
item.setCategory(catCombo.getValue());
|
||||
item.setSubCategory(subCatCombo.getValue());
|
||||
item.setName(ref(nameField));
|
||||
item.setDescription(ref(descField));
|
||||
String nameKey = nameLabel.getText().trim();
|
||||
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.setDamage(damageSpinner.getValue());
|
||||
String mr = modelRefField.getText().trim();
|
||||
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
||||
item.setConsumable(consumableCheck.isSelected());
|
||||
@@ -378,14 +425,16 @@ public class ItemEditorView extends BorderPane {
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
catCombo.setValue(null); // setzt subCatCombo via Listener zurück
|
||||
catCombo.setValue(null);
|
||||
subCatCombo.setValue(null);
|
||||
nameField.clear();
|
||||
descField.clear();
|
||||
nameLabel.setText("");
|
||||
descLabel.setText("");
|
||||
goldSpinner.getValueFactory().setValue(0);
|
||||
damageSpinner.getValueFactory().setValue(0);
|
||||
modelRefField.clear();
|
||||
consumableCheck.setSelected(false);
|
||||
effectsRows.getChildren().clear();
|
||||
updateWeaponSectionVisibility();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
@@ -453,9 +502,4 @@ public class ItemEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
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,
|
||||
SECRET,
|
||||
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. */
|
||||
public String displayName() {
|
||||
@@ -61,7 +91,69 @@ public enum AnimationAction {
|
||||
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
|
||||
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
||||
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)");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ public class KeyBindings {
|
||||
public int inventory = KeyInput.KEY_I;
|
||||
public int quicksave = KeyInput.KEY_F5;
|
||||
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. */
|
||||
public static final String[][] ENTRIES = {
|
||||
@@ -34,8 +37,11 @@ public class KeyBindings {
|
||||
{"turnRight", "Kamera rechts"},
|
||||
{"inventory", "Inventar"},
|
||||
{"quicksave", "Schnellspeichern"},
|
||||
{"character", "Charakter"},
|
||||
{"quests", "Questlog"},
|
||||
{"character", "Charakter"},
|
||||
{"quests", "Questlog"},
|
||||
{"drawMeleeWeapon", "Nahkampfwaffe ziehen"},
|
||||
{"drawRangedWeapon", "Fernkampfwaffe ziehen"},
|
||||
{"sheathWeapon", "Waffe wegstecken"},
|
||||
};
|
||||
|
||||
public int get(String fieldName) {
|
||||
|
||||
@@ -3,8 +3,12 @@ package de.blight.game.control;
|
||||
import com.jme3.anim.AnimComposer;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
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.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
@@ -35,8 +39,16 @@ public class PlayerInputControl {
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
private static final float ROTATE_SPEED = 10f;
|
||||
|
||||
private static final String[] ACTION_NAMES =
|
||||
{"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk"};
|
||||
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
|
||||
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 Camera cam;
|
||||
@@ -47,6 +59,17 @@ public class PlayerInputControl {
|
||||
private boolean forward, backward, left, right, sprint, walk;
|
||||
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 String animSetName;
|
||||
private Path assetRoot;
|
||||
@@ -139,6 +162,11 @@ public class PlayerInputControl {
|
||||
case "Sprint" -> sprint = isPressed;
|
||||
case "Walk" -> walk = isPressed;
|
||||
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 void setPaused(boolean paused) {
|
||||
@@ -219,6 +252,9 @@ public class PlayerInputControl {
|
||||
autopilotDir = null;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
stopWading();
|
||||
rightMouseHeld = false;
|
||||
combatAnimActive = false;
|
||||
combatAnimCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,13 +499,18 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void registerMappings(KeyBindings kb) {
|
||||
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
|
||||
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
|
||||
inputManager.addMapping("Left", new KeyTrigger(kb.left));
|
||||
inputManager.addMapping("Right", new KeyTrigger(kb.right));
|
||||
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
|
||||
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
|
||||
inputManager.addMapping("Walk", new KeyTrigger(kb.walk));
|
||||
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
|
||||
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
|
||||
inputManager.addMapping("Left", new KeyTrigger(kb.left));
|
||||
inputManager.addMapping("Right", new KeyTrigger(kb.right));
|
||||
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
|
||||
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -598,9 +639,9 @@ public class PlayerInputControl {
|
||||
|
||||
if (moving) {
|
||||
scratchMoveDir.normalizeLocal();
|
||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||
: sprint ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||
: (sprint && !weaponDrawn) ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
|
||||
|
||||
if (visual != null) {
|
||||
@@ -613,24 +654,44 @@ public class PlayerInputControl {
|
||||
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
|
||||
if (jumpFrames > 0) jumpFrames--;
|
||||
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||
|
||||
AnimationAction target;
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
} else if (moving) {
|
||||
target = (inDeepWater || walk) ? AnimationAction.WALK
|
||||
: sprint ? AnimationAction.SPRINT
|
||||
: AnimationAction.RUN;
|
||||
} else {
|
||||
target = AnimationAction.IDLE;
|
||||
}
|
||||
if (!combatAnimActive) {
|
||||
AnimationAction target;
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
} else if (moving) {
|
||||
target = (inDeepWater || walk) ? AnimationAction.WALK
|
||||
: sprint ? AnimationAction.SPRINT
|
||||
: AnimationAction.RUN;
|
||||
} else {
|
||||
target = AnimationAction.IDLE;
|
||||
}
|
||||
|
||||
if (target != currentAnim) {
|
||||
playAction(target);
|
||||
currentAnim = target;
|
||||
target = resolveWeaponAction(target);
|
||||
|
||||
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
|
||||
target = getMeleeBlockAction(drawnWeaponType);
|
||||
}
|
||||
|
||||
if (target != currentAnim) {
|
||||
playAction(target);
|
||||
currentAnim = target;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
java.util.function.BiFunction<Float, Float, float[]> query) {
|
||||
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();
|
||||
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
|
||||
de.blight.game.state.SaveGameState saveState =
|
||||
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.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseAxisTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
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.XPHelper;
|
||||
import de.blight.common.model.abilities.Abilities;
|
||||
@@ -46,9 +49,10 @@ public class CharacterState extends OverlayState {
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final String MAP_TOGGLE = "_CharToggle";
|
||||
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
||||
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
||||
private static final String MAP_TOGGLE = "_CharToggle";
|
||||
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
||||
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
||||
private static final String MAP_EQUIP_CLICK = "_CharEquipClick";
|
||||
|
||||
// ── Scroll-Zustand ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -64,6 +68,11 @@ public class CharacterState extends OverlayState {
|
||||
|
||||
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) {}
|
||||
|
||||
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_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
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
|
||||
@@ -89,13 +100,18 @@ public class CharacterState extends OverlayState {
|
||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
|
||||
app.getInputManager().deleteMapping(MAP_EQUIP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildContent(Node panel, float scale) {
|
||||
meleeSlotBounds = null;
|
||||
shieldSlotBounds = null;
|
||||
rangedSlotBounds = null;
|
||||
float divX = PX + PAD + LEFT_W + 6f;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -120,9 +136,22 @@ public class CharacterState extends OverlayState {
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
private void buildStats(Node panel) {
|
||||
private float buildStats(Node panel) {
|
||||
float x = PX + 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, "Stamina", stReg, x, y); y -= 16;
|
||||
regenLine(panel, "Mana", mnReg, x, y);
|
||||
return y;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── 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) {
|
||||
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
|
||||
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
|
||||
@@ -225,16 +225,43 @@ public class InventoryState extends OverlayState {
|
||||
|
||||
// ── 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() {
|
||||
gridNode = new Node("inv-grid");
|
||||
panel.attachChild(gridNode);
|
||||
|
||||
float savedContentTop = contentTop;
|
||||
if (activeTab == ItemCategory.WEAPON) {
|
||||
buildWeaponSlots(gridNode);
|
||||
contentTop -= 2 * (WEAPON_SLOT_H + 4);
|
||||
}
|
||||
|
||||
if (activeItems.isEmpty()) {
|
||||
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
|
||||
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
|
||||
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
|
||||
empty.setLocalTranslation(ex, ey, -17);
|
||||
gridNode.attachChild(empty);
|
||||
contentTop = savedContentTop;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,6 +305,7 @@ public class InventoryState extends OverlayState {
|
||||
if (col != 0) { virtY -= CELL_GAP; }
|
||||
virtY -= CELL_GAP;
|
||||
}
|
||||
contentTop = savedContentTop;
|
||||
}
|
||||
|
||||
private void rebuildGrid() {
|
||||
@@ -297,7 +325,8 @@ public class InventoryState extends OverlayState {
|
||||
int rows = (int) Math.ceil(g.size() / (double) COLS);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -392,6 +421,9 @@ public class InventoryState extends OverlayState {
|
||||
case STAFF -> "Stäbe";
|
||||
case HALBERD -> "Hellebarden";
|
||||
case AXE -> "Äxte";
|
||||
case BOW -> "Bögen";
|
||||
case CROSSBOW -> "Armbrüste";
|
||||
case MAGIC_CRYSTAL -> "Magiekristalle";
|
||||
case RING -> "Ringe";
|
||||
case NECKLACE -> "Halsketten";
|
||||
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 {
|
||||
SaveGameIO.save(save);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -267,6 +267,7 @@ public class WorldInteractableState extends BaseAppState {
|
||||
// ── Annäherung ────────────────────────────────────────────────────────────
|
||||
|
||||
private void startApproach(int idx) {
|
||||
if (playerInput.isWeaponDrawn()) return;
|
||||
targetIdx = idx;
|
||||
walkTimer = 0f;
|
||||
originPos = physicsChar.getPhysicsLocation().clone();
|
||||
|
||||
@@ -173,6 +173,20 @@ public class WorldItemsState extends BaseAppState {
|
||||
}
|
||||
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,
|
||||
@@ -421,6 +435,7 @@ public class WorldItemsState extends BaseAppState {
|
||||
// ── Pickup-Logik ──────────────────────────────────────────────────────────
|
||||
|
||||
private void tryPickup() {
|
||||
if (playerInput.isWeaponDrawn()) return;
|
||||
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
|
||||
if (hoveredIdx < 0 || physicsChar == null) return;
|
||||
|
||||
|
||||
@@ -147,6 +147,8 @@ quest.kuehles_bier.success
|
||||
quest.magnetiterz.description
|
||||
quest.magnetiterz.name
|
||||
quest.magnetiterz.success
|
||||
rostiges_kurzschwert.description
|
||||
rostiges_kurzschwert.name
|
||||
silas.name
|
||||
strandgut.description
|
||||
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