diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index e626a1c..fd4b8b3 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -1048,6 +1048,16 @@ public class SharedInput { public final ConcurrentLinkedQueue pathNetworkClickQueue = new ConcurrentLinkedQueue<>(); + /** + * JFX → JME: Drag-Ereignis im Wegnetz-Modus (Knoten verschieben). + * phase: START=0, MOVE=1, END=2. + */ + public record PathNetworkDrag(float screenX, float screenY, int phase) { + public static final int START = 0, MOVE = 1, END = 2; + } + public final ConcurrentLinkedQueue pathNetworkDragQueue = + new ConcurrentLinkedQueue<>(); + /** JME → JFX: Wegnetz wurde verändert, UI-Refresh nötig. */ public volatile boolean pathNetworkChanged = false; diff --git a/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java index eb5bd28..11f7b47 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PathNetworkEditorState.java @@ -34,25 +34,22 @@ import java.util.List; * *

Interaktionsmodell: *

- * - * Änderungen werden sofort in die Datei geschrieben. */ public class PathNetworkEditorState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(PathNetworkEditorState.class); - /** Radius in Welt-Einheiten, innerhalb dessen ein Klick einen Knoten trifft. */ - private static final float NODE_HIT_RADIUS = 3f; + private static final float NODE_HIT_RADIUS = 3f; private static final float NODE_SPHERE_RADIUS = 0.6f; - private static final float EDGE_RADIUS = 0.15f; + private static final float EDGE_RADIUS = 0.15f; + private static final float EDGE_HIT_THRESHOLD = 3f; private final SharedInput input; private Camera cam; @@ -60,13 +57,13 @@ public class PathNetworkEditorState extends BaseAppState { private Node rootNode; private TerrainQuad terrain; - private final PathNetwork network = new PathNetwork(); + private final PathNetwork network = new PathNetwork(); private PathNode selectedNode = null; + private PathNode draggedNode = null; - // Szene-Graph-Knoten für Visualisierung - private final Node netRoot = new Node("pathnet_root"); - private final Node nodeRoot = new Node("pathnet_nodes"); - private final Node edgeRoot = new Node("pathnet_edges"); + private final Node netRoot = new Node("pathnet_root"); + private final Node nodeRoot = new Node("pathnet_nodes"); + private final Node edgeRoot = new Node("pathnet_edges"); private Material matDefault; private Material matSelected; @@ -83,9 +80,9 @@ public class PathNetworkEditorState extends BaseAppState { assets = app.getAssetManager(); rootNode = app.getRootNode(); - matDefault = mat(new ColorRGBA(1f, 0.85f, 0f, 1f)); // gelb - matSelected = mat(new ColorRGBA(1f, 0.4f, 0f, 1f)); // orange - matEdge = mat(new ColorRGBA(0.3f, 0.9f, 0.3f, 1f)); // grün + matDefault = mat(new ColorRGBA(1f, 0.85f, 0f, 1f)); + matSelected = mat(new ColorRGBA(1f, 0.4f, 0f, 1f)); + matEdge = mat(new ColorRGBA(0.3f, 0.9f, 0.3f, 1f)); netRoot.attachChild(nodeRoot); netRoot.attachChild(edgeRoot); @@ -111,6 +108,7 @@ public class PathNetworkEditorState extends BaseAppState { protected void onDisable() { netRoot.removeFromParent(); selectedNode = null; + draggedNode = null; } public void setTerrain(TerrainQuad t) { this.terrain = t; } @@ -123,21 +121,31 @@ public class PathNetworkEditorState extends BaseAppState { while ((click = input.pathNetworkClickQueue.poll()) != null) { handleClick(click); } + + // MOVE-Events kollabieren: nur den aktuellsten pro Frame verarbeiten, + // START/END sofort durchlassen. + SharedInput.PathNetworkDrag pendingMove = null; + SharedInput.PathNetworkDrag drag; + while ((drag = input.pathNetworkDragQueue.poll()) != null) { + if (drag.phase() == SharedInput.PathNetworkDrag.MOVE) { + pendingMove = drag; + } else { + if (pendingMove != null) { handleDrag(pendingMove); pendingMove = null; } + handleDrag(drag); + } + } + if (pendingMove != null) { handleDrag(pendingMove); } + if (input.deletePathNodeRequested) { input.deletePathNodeRequested = false; deleteSelected(); } } + // ── Klick ───────────────────────────────────────────────────────────────── + private void handleClick(SharedInput.PathNetworkClick click) { - float jmeX = click.screenX() * (float) input.viewportScaleX; - float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; - - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - - // Knoten in der Nähe des Strahls suchen + Ray ray = screenToRay(click.screenX(), click.screenY()); PathNode hitNode = findNodeNearRay(ray); if (click.rightClick()) { @@ -149,35 +157,37 @@ public class PathNetworkEditorState extends BaseAppState { private void handleLeftClick(Ray ray, PathNode hitNode) { if (hitNode != null) { - // Klick auf Knoten if (selectedNode == null) { select(hitNode); } else if (selectedNode == hitNode) { - select(null); // Abwählen + select(null); } else { network.addEdge(selectedNode.getUuid(), hitNode.getUuid()); select(hitNode); saveAndNotify(); rebuildVisuals(); } - } else { - // Klick auf Terrain → neuer Knoten - WorldPoint pt = terrainHit(ray); - if (pt == null) return; - PathNode newNode = new PathNode(pt); - network.addNode(newNode); - if (selectedNode != null) { - network.addEdge(selectedNode.getUuid(), newNode.getUuid()); - } - select(newNode); - saveAndNotify(); - rebuildVisuals(); + return; } + + WorldPoint pt = terrainHit(ray); + if (pt == null) return; + + // Kante in der Nähe? → splitten und neuen Knoten einfügen + PathEdge edgeHit = findEdgeNearPoint(pt); + PathNode newNode = (edgeHit != null) ? insertNodeOnEdge(edgeHit, pt) : newNodeAt(pt); + + if (selectedNode != null) { + network.addEdge(selectedNode.getUuid(), newNode.getUuid()); + } + select(newNode); + saveAndNotify(); + rebuildVisuals(); } private void handleRightClick(PathNode hitNode) { if (hitNode != null) { - if (selectedNode == hitNode) selectedNode = null; + if (selectedNode == hitNode) { selectedNode = null; } network.removeNode(hitNode.getUuid()); saveAndNotify(); rebuildVisuals(); @@ -186,6 +196,83 @@ public class PathNetworkEditorState extends BaseAppState { } } + // ── Drag (Knoten verschieben) ────────────────────────────────────────────── + + private void handleDrag(SharedInput.PathNetworkDrag drag) { + Ray ray = screenToRay(drag.screenX(), drag.screenY()); + + switch (drag.phase()) { + case SharedInput.PathNetworkDrag.START -> { + PathNode node = findNodeNearRay(ray); + if (node != null) { + draggedNode = node; + select(node); + } + } + case SharedInput.PathNetworkDrag.MOVE -> { + if (draggedNode != null) { + WorldPoint pt = terrainHit(ray); + if (pt != null) { + draggedNode.setPosition(pt); + moveNodeVisual(draggedNode); + } + } + } + case SharedInput.PathNetworkDrag.END -> { + if (draggedNode != null) { + WorldPoint pt = terrainHit(ray); + if (pt != null) { draggedNode.setPosition(pt); } + saveAndNotify(); + rebuildVisuals(); + draggedNode = null; + } + } + } + } + + // ── Kanten-Insertion ─────────────────────────────────────────────────────── + + private PathEdge findEdgeNearPoint(WorldPoint pt) { + PathEdge best = null; + float bestDist = EDGE_HIT_THRESHOLD; + for (PathEdge e : network.getEdges()) { + PathNode a = network.nodeById(e.getNodeUuidA()); + PathNode b = network.nodeById(e.getNodeUuidB()); + if (a == null || b == null) continue; + float d = distPointToSegmentXZ(pt, a.getPosition(), b.getPosition()); + if (d < bestDist) { bestDist = d; best = e; } + } + return best; + } + + private float distPointToSegmentXZ(WorldPoint p, WorldPoint a, WorldPoint b) { + float dx = b.x - a.x, dz = b.z - a.z; + float len2 = dx * dx + dz * dz; + if (len2 < 0.001f) { + float ex = p.x - a.x, ez = p.z - a.z; + return (float) Math.sqrt(ex * ex + ez * ez); + } + float t = Math.max(0f, Math.min(1f, ((p.x - a.x) * dx + (p.z - a.z) * dz) / len2)); + float cx = a.x + t * dx, cz = a.z + t * dz; + float ex = p.x - cx, ez = p.z - cz; + return (float) Math.sqrt(ex * ex + ez * ez); + } + + private PathNode insertNodeOnEdge(PathEdge edge, WorldPoint pos) { + PathNode newNode = new PathNode(pos); + network.addNode(newNode); + network.removeEdge(edge.getNodeUuidA(), edge.getNodeUuidB()); + network.addEdge(edge.getNodeUuidA(), newNode.getUuid()); + network.addEdge(newNode.getUuid(), edge.getNodeUuidB()); + return newNode; + } + + private PathNode newNodeAt(WorldPoint pos) { + PathNode n = new PathNode(pos); + network.addNode(n); + return n; + } + // ── Visualisierung ───────────────────────────────────────────────────────── private void rebuildVisuals() { @@ -193,8 +280,7 @@ public class PathNetworkEditorState extends BaseAppState { edgeRoot.detachAllChildren(); for (PathNode n : network.getNodes()) { - Geometry g = new Geometry("node_" + n.getUuid(), - new Sphere(8, 8, NODE_SPHERE_RADIUS)); + Geometry g = new Geometry("node_" + n.getUuid(), new Sphere(8, 8, NODE_SPHERE_RADIUS)); g.setMaterial(n == selectedNode ? matSelected : matDefault); g.setLocalTranslation(n.getPosition().x, n.getPosition().y + NODE_SPHERE_RADIUS, n.getPosition().z); g.setUserData("nodeUuid", n.getUuid()); @@ -210,6 +296,27 @@ public class PathNetworkEditorState extends BaseAppState { } } + /** Effizienter Update: nur Sphere-Position + betroffene Kanten neu bauen. */ + private void moveNodeVisual(PathNode node) { + Geometry sphere = (Geometry) nodeRoot.getChild("node_" + node.getUuid()); + if (sphere != null) { + sphere.setLocalTranslation( + node.getPosition().x, node.getPosition().y + NODE_SPHERE_RADIUS, node.getPosition().z); + } + String uuid = node.getUuid(); + for (PathEdge e : network.getEdges()) { + if (!e.involves(uuid)) continue; + PathNode a = network.nodeById(e.getNodeUuidA()); + PathNode b = network.nodeById(e.getNodeUuidB()); + if (a == null || b == null) continue; + String edgeName = "edge_" + a.getUuid() + "_" + b.getUuid(); + Spatial old = edgeRoot.getChild(edgeName); + if (old != null) { old.removeFromParent(); } + Geometry g = buildEdgeGeometry(a, b); + if (g != null) { edgeRoot.attachChild(g); } + } + } + private Geometry buildEdgeGeometry(PathNode a, PathNode b) { float ax = a.getPosition().x, ay = a.getPosition().y, az = a.getPosition().z; float bx = b.getPosition().x, by = b.getPosition().y, bz = b.getPosition().z; @@ -225,26 +332,30 @@ public class PathNetworkEditorState extends BaseAppState { g.setMaterial(matEdge); g.setLocalTranslation(mid); - // Zylinder liegt entlang lokaler Y-Achse. - // Rotationsmatrix: Spalten = wohin lokale Achsen X, Y, Z in Weltkoordinaten zeigen. - // Lokale Y → Richtung des Weges (nx, 0, nz) im XZ-Boden. - // Lokale Z → Welt-Y (oben), damit Zylinder nicht rollt. - // Lokale X = lokale Y × lokale Z = (nz, 0, -nx). Matrix3f rot = new Matrix3f(); - rot.setColumn(0, new Vector3f( nz, 0f, -nx)); // lokales X - rot.setColumn(1, new Vector3f( nx, 0f, nz)); // lokales Y → dir - rot.setColumn(2, new Vector3f( 0f, 1f, 0f)); // lokales Z → Welt-oben + rot.setColumn(0, new Vector3f( nz, 0f, -nx)); + rot.setColumn(1, new Vector3f( nx, 0f, nz)); + rot.setColumn(2, new Vector3f( 0f, 1f, 0f)); g.setLocalRotation(new Quaternion().fromRotationMatrix(rot)); return g; } private void updateNodeMaterial(PathNode node) { Geometry g = (Geometry) nodeRoot.getChild("node_" + node.getUuid()); - if (g != null) g.setMaterial(node == selectedNode ? matSelected : matDefault); + if (g != null) { g.setMaterial(node == selectedNode ? matSelected : matDefault); } } // ── Hilfsmethoden ────────────────────────────────────────────────────────── + private Ray screenToRay(float screenX, float screenY) { + float jmeX = screenX * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY; + Vector2f sc = new Vector2f(jmeX, jmeY); + Vector3f near = cam.getWorldCoordinates(sc, 0f); + Vector3f far = cam.getWorldCoordinates(sc, 1f); + return new Ray(near, far.subtract(near).normalizeLocal()); + } + private PathNode findNodeNearRay(Ray ray) { PathNode best = null; float bestDist = NODE_HIT_RADIUS; @@ -273,8 +384,8 @@ public class PathNetworkEditorState extends BaseAppState { private void select(PathNode node) { PathNode prev = selectedNode; selectedNode = node; - if (prev != null) updateNodeMaterial(prev); - if (node != null) updateNodeMaterial(node); + if (prev != null) { updateNodeMaterial(prev); } + if (node != null) { updateNodeMaterial(node); } } private void saveAndNotify() { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java index 90a23ef..f1b5ef4 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java @@ -24,6 +24,12 @@ public class ViewportController { private int editPressAction; private javafx.animation.Timeline editTimer; + // Wegnetz-Drag-Tracking + private float pathNetPressX, pathNetPressY; + private boolean pathNetPressPending; + private boolean pathNetDragging; + private boolean pathNetRightPress; + public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) { this.input = input; this.initialImage = jfxImage; @@ -140,12 +146,12 @@ public class ViewportController { } if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { - if (e.getButton() == MouseButton.PRIMARY && !bothDown) { - input.pathNetworkClickQueue.offer( - new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false)); - } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { - input.pathNetworkClickQueue.offer( - new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true)); + if (!bothDown && (e.getButton() == MouseButton.PRIMARY || e.getButton() == MouseButton.SECONDARY)) { + pathNetPressX = (float) e.getX(); + pathNetPressY = (float) e.getY(); + pathNetPressPending = true; + pathNetDragging = false; + pathNetRightPress = e.getButton() == MouseButton.SECONDARY; } return; } @@ -225,6 +231,25 @@ public class ViewportController { return; } + if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK + && e.isPrimaryButtonDown() && !pathNetRightPress) { + if (pathNetPressPending) { + float ddx = (float)(e.getX() - pathNetPressX); + float ddy = (float)(e.getY() - pathNetPressY); + if (ddx * ddx + ddy * ddy > 16f) { + input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag( + pathNetPressX, pathNetPressY, SharedInput.PathNetworkDrag.START)); + pathNetDragging = true; + pathNetPressPending = false; + } + } + if (pathNetDragging) { + input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag( + (float) e.getX(), (float) e.getY(), SharedInput.PathNetworkDrag.MOVE)); + } + return; + } + // Play-Tool EDIT: Drag-Events an JME3 weiterleiten if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL && input.playToolMode == SharedInput.PlayToolMode.EDIT @@ -245,6 +270,17 @@ public class ViewportController { viewport.setOnMouseReleased(e -> { objDragging = false; if (!isObjectMode()) { stopEditTimer(); } + if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { + if (pathNetDragging) { + input.pathNetworkDragQueue.offer(new SharedInput.PathNetworkDrag( + (float) e.getX(), (float) e.getY(), SharedInput.PathNetworkDrag.END)); + pathNetDragging = false; + } else if (pathNetPressPending) { + input.pathNetworkClickQueue.offer(new SharedInput.PathNetworkClick( + pathNetPressX, pathNetPressY, pathNetRightPress)); + pathNetPressPending = false; + } + } if (input.activeLayer == SharedInput.LAYER_WATER) { input.waterDragEnd = true; } diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java index 52e7436..f8f48d9 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java @@ -30,7 +30,8 @@ public enum AnimationAction { TALK2, DISAPPOINT, SECRET, - TALK_SITTING; + TALK_SITTING, + SWORD_ATTACK_1; /** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */ public String displayName() { @@ -60,6 +61,7 @@ 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)"); }; } } diff --git a/blight-map/src/main/map/blight_paths.blp b/blight-map/src/main/map/blight_paths.blp index 022f0e3..78ae41e 100644 --- a/blight-map/src/main/map/blight_paths.blp +++ b/blight-map/src/main/map/blight_paths.blp @@ -9,17 +9,27 @@ NODE d886a17b-4ad0-4437-9dbf-6c0df951742f - 252.24966 6.40171 -892.78693 NODE caa8813b-d828-4db8-ae77-9cf369309618 - 254.10876 2.00000 -852.84991 NODE ff5bb246-2c95-4e76-b592-5881c51dab51 - 254.28955 2.00035 -826.95752 NODE 28a87ffd-a39b-4347-a79f-fe5ead071b59 - 291.11404 2.00004 -817.18658 -NODE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 - 306.78833 2.00000 -777.88641 +NODE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 - 306.97855 2.00000 -784.28113 NODE c29223fa-67a0-4df0-8055-91381ec8c83f - 317.70370 2.00000 -782.45502 NODE 43677d9c-a19b-45f2-8eb3-d3368251d0b9 - 349.32700 2.00000 -774.56525 +NODE aeb4a96f-b1d7-404a-a654-e98f74e5c856 - 299.91858 2.00001 -803.26099 +NODE 64a2b232-6180-4f6d-9984-82820ac79053 - 298.20966 2.00000 -787.21967 +NODE 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 - 288.40808 2.00000 -778.93536 +NODE 2b4e0e77-30a4-4314-87ad-0d3582feffd0 - 280.04413 2.00000 -766.30432 +NODE 4f3a890b-a973-41fc-9798-d897209b12d0 - 295.18164 2.00000 -735.89783 EDGE c7fe03f3-736b-4d81-accb-27dd6649e000 fa3be430-7714-49ed-9a81-4e4fb45ba564 EDGE fa3be430-7714-49ed-9a81-4e4fb45ba564 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 d886a17b-4ad0-4437-9dbf-6c0df951742f EDGE caa8813b-d828-4db8-ae77-9cf369309618 ff5bb246-2c95-4e76-b592-5881c51dab51 EDGE ff5bb246-2c95-4e76-b592-5881c51dab51 28a87ffd-a39b-4347-a79f-fe5ead071b59 -EDGE 28a87ffd-a39b-4347-a79f-fe5ead071b59 7e3c1080-d37e-4fc9-b489-c0784f34fac1 EDGE 7e3c1080-d37e-4fc9-b489-c0784f34fac1 c29223fa-67a0-4df0-8055-91381ec8c83f EDGE c29223fa-67a0-4df0-8055-91381ec8c83f 43677d9c-a19b-45f2-8eb3-d3368251d0b9 EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 f8f01e21-3963-4530-a5d6-bfdd0cb77233 EDGE 5618a5ba-d0a5-4897-ab4d-a4a2932edb18 caa8813b-d828-4db8-ae77-9cf369309618 EDGE f8f01e21-3963-4530-a5d6-bfdd0cb77233 caa8813b-d828-4db8-ae77-9cf369309618 +EDGE 28a87ffd-a39b-4347-a79f-fe5ead071b59 aeb4a96f-b1d7-404a-a654-e98f74e5c856 +EDGE aeb4a96f-b1d7-404a-a654-e98f74e5c856 64a2b232-6180-4f6d-9984-82820ac79053 +EDGE 64a2b232-6180-4f6d-9984-82820ac79053 7e3c1080-d37e-4fc9-b489-c0784f34fac1 +EDGE 64a2b232-6180-4f6d-9984-82820ac79053 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 +EDGE 0bcd1776-f65a-4d2f-9f2f-b7cff6db8dc6 2b4e0e77-30a4-4314-87ad-0d3582feffd0 +EDGE 2b4e0e77-30a4-4314-87ad-0d3582feffd0 4f3a890b-a973-41fc-9798-d897209b12d0