diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiDiagram.java b/src/main/java/noppes/npcs/client/gui/global/GuiDiagram.java index 60b5def7b..2c0ee2fac 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiDiagram.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiDiagram.java @@ -12,74 +12,73 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; /** - * Abstract GUI class for drawing diagrams that consist of icons (nodes) and connections (arrows). - *

- * This version adds: - * - A parent GuiNPCInterface (passed in the constructor) so that if the parent has a subgui, no - * mouse interactions are processed. - * - Caching for icons and connections. The abstract methods createIcons() and createConnections() are - * called only once (or when cache is invalidated). - * - DiagramIcon now has fields for enabled and pressable. Disabled icons are not rendered or used in - * connection validation. - * - Basic mouse-event callbacks for pressable icons: click, held and released. + * Abstract GUI class for drawing diagrams with icons (nodes) and connections (arrows). + * + * Features: + * - Uses a parent GuiNPCInterface to disable mouse interaction when a subgui is active. + * - Caches icons and connections (subclasses must implement createIcons() and createConnections()). + * - DiagramIcon now has an index (for ring, row, or level) and a priority (for ordering). + * - New manual layout types (CIRCULAR_MANUAL, SQUARE_MANUAL, TREE_MANUAL) use index and priority + * to order nodes. + * - Arrow-drawing options: curvedArrows (if false, straight lines) and curveAngle (in degrees). + * - For curved arrows, hit-detection is done by approximating the quadratic Bezier with 20 segments. */ public abstract class GuiDiagram extends Gui { - // --------------- // Diagram Area & Controls - // --------------- - protected int x, y, width, height; // The drawing area. - protected float panX = 0, panY = 0; // Panning offsets. - protected float zoom = 1.0f; // Zoom factor. - protected boolean dragging = false; // Drag flag. - protected int lastDragX, lastDragY; // Last mouse positions. + protected int x, y, width, height; + protected float panX = 0, panY = 0; + protected float zoom = 1.0f; + protected boolean dragging = false; + protected int lastDragX, lastDragY; - // --------------- // Node (Icon) Sizing - // --------------- - protected int iconSize = 16; // Icon image size (square). - protected int slotPadding = 4; // Extra padding for the node's box. - protected int slotSize = iconSize + slotPadding; // Overall size of the node box. + protected int iconSize = 16; + protected int slotPadding = 4; + protected int slotSize = iconSize + slotPadding; - // --------------- // Node Box Appearance - // --------------- - protected int iconBackgroundColor = 0xFF999999; // Background color. - protected int iconBorderColor = 0xFF555555; // Border color. - protected int iconBorderThickness = 1; // Border thickness in pixels. + protected int iconBackgroundColor = 0xFF999999; + protected int iconBorderColor = 0xFF555555; + protected int iconBorderThickness = 1; - // --------------- // Connection Options - // --------------- - protected boolean showArrowHeads = true; // Whether to draw arrow heads. - protected boolean useColorScaling = true; // Whether to scale connection color based on a percentage. + protected boolean showArrowHeads = true; + protected boolean useColorScaling = true; - // --------------- // Layout Options - // --------------- - public enum DiagramLayout { CIRCULAR, SQUARE, TREE, GENERATED } - protected DiagramLayout layout = DiagramLayout.CIRCULAR; // Default layout. + public enum DiagramLayout { + CIRCULAR, SQUARE, TREE, GENERATED, + CIRCULAR_MANUAL, SQUARE_MANUAL, TREE_MANUAL + } + protected DiagramLayout layout = DiagramLayout.CIRCULAR; /** - * Allows setting the desired layout type. - * This will invalidate the cached positions and cached icons/connections. + * Sets the layout and invalidates caches. */ public void setLayout(DiagramLayout layout) { this.layout = layout; invalidateCache(); } - // --------------- - // Caching - // --------------- - // Cache for positions (computed once for any given icon set & layout). + // Arrow Drawing Options + protected boolean curvedArrows = true; // if false, straight lines are drawn. + protected int curveAngle = 15; // in degrees; adjust for "hardness" of curve. + + public void setCurvedArrows(boolean curved) { + this.curvedArrows = curved; + } + public void setCurveAngle(int angle) { + this.curveAngle = angle; + } + + // Caching for positions and for icons/connections. protected Map cachedPositions = null; - // Cache for icons and connections. protected List iconsCache = null; protected List connectionsCache = null; /** - * Clears all caches: positions, icons, and connections. + * Clears all caches. */ public void invalidateCache() { cachedPositions = null; @@ -87,14 +86,10 @@ public abstract class GuiDiagram extends Gui { connectionsCache = null; } - // --------------- // Parent Reference - // --------------- protected GuiNPCInterface parent; - // --------------- // Constructor - // --------------- public GuiDiagram(GuiNPCInterface parent, int x, int y, int width, int height) { this.parent = parent; this.x = x; @@ -103,34 +98,16 @@ public abstract class GuiDiagram extends Gui { this.height = height; } - // --------------- - // Data Providers (subclasses must implement the creation methods) - // --------------- - /** - * Subclasses should override this method to create and return the list of DiagramIcons. - * Note: the returned icons will be cached. - */ + // Data Providers – Subclasses must implement these. protected abstract List createIcons(); - - /** - * Subclasses should override this method to create and return the list of DiagramConnections. - * Note: the returned connections will be cached. - */ protected abstract List createConnections(); - /** - * Returns the cached icons. - */ protected final List getIcons() { if (iconsCache == null) { iconsCache = createIcons(); } return iconsCache; } - - /** - * Returns the cached connections. - */ protected final List getConnections() { if (connectionsCache == null) { connectionsCache = createConnections(); @@ -139,15 +116,11 @@ public abstract class GuiDiagram extends Gui { } /** - * Subclasses must implement how an icon’s image is rendered. + * Subclasses must implement how an icon is rendered. */ protected abstract void renderIcon(DiagramIcon icon, int posX, int posY, IconRenderState state); - /** - * Return a tooltip for an icon. - */ protected List getIconTooltip(DiagramIcon icon) { return null; } - protected List getConnectionTooltip(DiagramConnection conn) { List tooltip = new ArrayList<>(); DiagramIcon iconFrom = getIconById(conn.idFrom); @@ -163,51 +136,35 @@ public abstract class GuiDiagram extends Gui { } return tooltip; } - protected String getIconName(DiagramIcon icon) { return "Icon " + icon.id; } - // --------------- - // Layout Calculation – choose algorithm based on layout type. - // --------------- - /** - * Returns a mapping from each icon's id to its (x,y) center position. - * Uses caching so that positions are computed only once (unless invalidated). - */ + // Layout Calculation – Chooses algorithm based on layout type. protected Map calculatePositions() { - if (cachedPositions != null) { - return cachedPositions; - } + if (cachedPositions != null) return cachedPositions; switch (layout) { - case CIRCULAR: - cachedPositions = calculateCircularPositions(); - break; - case SQUARE: - cachedPositions = calculateSquarePositions(); - break; - case TREE: - cachedPositions = calculateTreePositions(); - break; - case GENERATED: - cachedPositions = calculateGeneratedPositions(); - break; - default: - cachedPositions = calculateCircularPositions(); - break; + case CIRCULAR: cachedPositions = calculateCircularPositions(); break; + case SQUARE: cachedPositions = calculateSquarePositions(); break; + case TREE: cachedPositions = calculateTreePositions(); break; + case GENERATED: cachedPositions = calculateGeneratedPositions(); break; + case CIRCULAR_MANUAL: cachedPositions = calculateCircularManualPositions(); break; + case SQUARE_MANUAL: cachedPositions = calculateSquareManualPositions(); break; + case TREE_MANUAL: cachedPositions = calculateTreeManualPositions(); break; + default: cachedPositions = calculateCircularPositions(); break; } return cachedPositions; } + // Standard Layouts: protected Map calculateCircularPositions() { List icons = getIcons(); Map positions = new HashMap<>(); int count = icons.size(); - int centerX = x + width / 2; - int centerY = y + height / 2; + int centerX = x + width / 2, centerY = y + height / 2; int radius = Math.min(width, height) / 2 - iconSize - 10; for (int i = 0; i < count; i++) { double angle = 2 * Math.PI * i / count; - int posX = centerX + (int)(radius * Math.cos(angle)); - int posY = centerY + (int)(radius * Math.sin(angle)); + int posX = centerX + (int) (radius * Math.cos(angle)); + int posY = centerY + (int) (radius * Math.sin(angle)); positions.put(icons.get(i).id, new Point(posX, posY)); } return positions; @@ -217,16 +174,12 @@ public abstract class GuiDiagram extends Gui { List icons = getIcons(); Map positions = new HashMap<>(); int count = icons.size(); - int n = (int)Math.ceil(Math.sqrt(count)); - int gridWidth = width - 20; - int gridHeight = height - 20; - int cellWidth = gridWidth / n; - int cellHeight = gridHeight / n; - int startX = x + (width - gridWidth) / 2; - int startY = y + (height - gridHeight) / 2; + int n = (int) Math.ceil(Math.sqrt(count)); + int gridWidth = width - 20, gridHeight = height - 20; + int cellWidth = gridWidth / n, cellHeight = gridHeight / n; + int startX = x + (width - gridWidth) / 2, startY = y + (height - gridHeight) / 2; for (int i = 0; i < count; i++) { - int row = i / n; - int col = i % n; + int row = i / n, col = i % n; int posX = startX + col * cellWidth + cellWidth / 2; int posY = startY + row * cellHeight + cellHeight / 2; positions.put(icons.get(i).id, new Point(posX, posY)); @@ -238,11 +191,11 @@ public abstract class GuiDiagram extends Gui { List icons = getIcons(); Map positions = new HashMap<>(); int count = icons.size(); - int levels = (int)Math.ceil(Math.log(count + 1) / Math.log(2)); + int levels = (int) Math.ceil(Math.log(count + 1) / Math.log(2)); int levelHeight = height / (levels + 1); int index = 0; for (int level = 0; level < levels && index < count; level++) { - int nodesThisLevel = (int)Math.min(Math.pow(2, level), count - index); + int nodesThisLevel = (int) Math.min(Math.pow(2, level), count - index); int levelWidth = width - 20; int cellWidth = levelWidth / nodesThisLevel; for (int i = 0; i < nodesThisLevel && index < count; i++, index++) { @@ -254,29 +207,17 @@ public abstract class GuiDiagram extends Gui { return positions; } - /** - * Generated layout: uses a force-directed algorithm to position nodes. - * Computed only once using a fixed seed for determinism. - */ protected Map calculateGeneratedPositions() { List icons = getIcons(); List connections = getConnections(); - - // Build a complete undirected graph from icons & connections. Map> graph = buildGraph(icons, connections); - - // Partition the graph into clusters (connected components). List> clusters = getConnectedComponents(graph); - - // Arrange clusters into a global layout. We’ll place each cluster - // in its own bounding box on a grid. int clusterCount = clusters.size(); int gridCols = (int) Math.ceil(Math.sqrt(clusterCount)); int gridRows = (int) Math.ceil((double) clusterCount / gridCols); int clusterMargin = 20; int compWidth = (width - (gridCols + 1) * clusterMargin) / gridCols; int compHeight = (height - (gridRows + 1) * clusterMargin) / gridRows; - Map globalPositions = new HashMap<>(); int clusterIndex = 0; for (Set cluster : clusters) { @@ -284,31 +225,22 @@ public abstract class GuiDiagram extends Gui { int row = clusterIndex / gridCols; int compX = x + clusterMargin + col * (compWidth + clusterMargin); int compY = y + clusterMargin + row * (compHeight + clusterMargin); - - // Look for a pattern in the cluster. Map compPositions; - if (isCycle(cluster, graph)) { - // Layout nodes evenly on a circle. + if (isCycle(cluster, graph)) compPositions = layoutCycle(cluster, compX, compY, compWidth, compHeight); - } else if (isTree(cluster, graph)) { - // Layout nodes in a tree, minimizing edge crossings. + else if (isTree(cluster, graph)) compPositions = layoutTree(cluster, compX, compY, compWidth, compHeight, graph); - } else if (isSquare(cluster)) { - // If the number of nodes is a perfect square (or near enough) - // you might try a grid layout. + else if (isSquare(cluster)) compPositions = layoutSquare(cluster, compX, compY, compWidth, compHeight); - } else { - // Otherwise, use a force-directed layout optimized for minimal overlap. + else compPositions = layoutForceDirected(cluster, compX, compY, compWidth, compHeight, connections); - } globalPositions.putAll(compPositions); clusterIndex++; } - return globalPositions; } - // Build an undirected graph from icons and connections. + // Helpers for generated layout. private Map> buildGraph(List icons, List connections) { Map> graph = new HashMap<>(); for (DiagramIcon icon : icons) { @@ -322,8 +254,6 @@ public abstract class GuiDiagram extends Gui { } return graph; } - - // Extract connected components using DFS. private List> getConnectedComponents(Map> graph) { Set visited = new HashSet<>(); List> components = new ArrayList<>(); @@ -336,9 +266,7 @@ public abstract class GuiDiagram extends Gui { } return components; } - - private void dfsComponent(Integer current, Map> graph, - Set visited, Set comp) { + private void dfsComponent(Integer current, Map> graph, Set visited, Set comp) { visited.add(current); comp.add(current); for (Integer neighbor : graph.get(current)) { @@ -347,11 +275,6 @@ public abstract class GuiDiagram extends Gui { } } } - - // Pattern detection helpers: - - // A cluster is considered a cycle if each node has exactly 2 neighbors (within the cluster) - // and there are at least 3 nodes. private boolean isCycle(Set cluster, Map> graph) { if (cluster.size() < 3) return false; for (Integer node : cluster) { @@ -363,8 +286,6 @@ public abstract class GuiDiagram extends Gui { } return true; } - - // A cluster is considered a tree if the number of edges equals nodes-1. private boolean isTree(Set cluster, Map> graph) { int edgeCount = 0; for (Integer node : cluster) { @@ -372,42 +293,34 @@ public abstract class GuiDiagram extends Gui { if (cluster.contains(neighbor)) edgeCount++; } } - edgeCount /= 2; // each edge counted twice. + edgeCount /= 2; return edgeCount == cluster.size() - 1; } - - // A simple heuristic: if the cluster size is a perfect square or nearly so, treat it as a square. private boolean isSquare(Set cluster) { int n = cluster.size(); int sqrt = (int) Math.round(Math.sqrt(n)); return sqrt * sqrt == n; } -// Layout algorithms: - - // Circular layout for cycles. + // Layout algorithms for clusters: private Map layoutCycle(Set cluster, int compX, int compY, int compWidth, int compHeight) { Map positions = new HashMap<>(); - int centerX = compX + compWidth / 2; - int centerY = compY + compHeight / 2; + int centerX = compX + compWidth / 2, centerY = compY + compHeight / 2; int radius = Math.min(compWidth, compHeight) / 3; int i = 0; for (Integer id : cluster) { double angle = 2 * Math.PI * i / cluster.size(); - int posX = centerX + (int)(radius * Math.cos(angle)); - int posY = centerY + (int)(radius * Math.sin(angle)); + int posX = centerX + (int) (radius * Math.cos(angle)); + int posY = centerY + (int) (radius * Math.sin(angle)); positions.put(id, new Point(posX, posY)); i++; } return positions; } - // A simple top-down tree layout using BFS. private Map layoutTree(Set cluster, int compX, int compY, int compWidth, int compHeight, Map> fullGraph) { Map positions = new HashMap<>(); - - // Build subgraph for the cluster. Map> subgraph = new HashMap<>(); for (Integer id : cluster) { List neighbors = new ArrayList<>(); @@ -417,15 +330,10 @@ public abstract class GuiDiagram extends Gui { } subgraph.put(id, neighbors); } - - // Choose a root (for example, the node with the smallest id). Integer root = cluster.iterator().next(); for (Integer id : cluster) { - if (id < root) - root = id; + if (id < root) root = id; } - - // BFS to determine levels. Map levelMap = new HashMap<>(); Map> levels = new HashMap<>(); Queue queue = new LinkedList<>(); @@ -433,7 +341,6 @@ public abstract class GuiDiagram extends Gui { queue.add(root); visited.add(root); levelMap.put(root, 0); - while (!queue.isEmpty()) { Integer curr = queue.poll(); int lvl = levelMap.get(curr); @@ -446,35 +353,30 @@ public abstract class GuiDiagram extends Gui { } } } - - // Calculate vertical spacing. int maxLevel = levels.keySet().stream().max(Integer::compare).orElse(0); double levelHeight = compHeight / (maxLevel + 1.0); - - // For each level, assign horizontal positions evenly. for (Map.Entry> entry : levels.entrySet()) { int lvl = entry.getKey(); List nodesAtLevel = entry.getValue(); - double horizontalSpacing = compWidth / (nodesAtLevel.size() + 1.0); + nodesAtLevel.sort(Comparator.comparingInt(a -> getIconById(a).priority)); + double spacing = compWidth / (nodesAtLevel.size() + 1.0); for (int i = 0; i < nodesAtLevel.size(); i++) { int id = nodesAtLevel.get(i); - int posX = compX + (int)((i + 1) * horizontalSpacing); - int posY = compY + (int)(lvl * levelHeight + levelHeight / 2); + int posX = compX + (int) ((i + 1) * spacing); + int posY = compY + (int) (lvl * levelHeight + levelHeight / 2); positions.put(id, new Point(posX, posY)); } } - return positions; } - // Grid layout for square-like clusters. private Map layoutSquare(Set cluster, int compX, int compY, int compWidth, int compHeight) { Map positions = new HashMap<>(); int n = (int) Math.round(Math.sqrt(cluster.size())); - int gridCellWidth = compWidth / n; - int gridCellHeight = compHeight / n; + int gridCellWidth = compWidth / n, gridCellHeight = compHeight / n; int index = 0; List nodes = new ArrayList<>(cluster); + nodes.sort(Comparator.comparingInt(a -> getIconById(a).priority)); for (int row = 0; row < n; row++) { for (int col = 0; col < n; col++) { if (index >= nodes.size()) break; @@ -487,7 +389,6 @@ public abstract class GuiDiagram extends Gui { return positions; } - // Force-directed layout for generic clusters. private Map layoutForceDirected(Set cluster, int compX, int compY, int compWidth, int compHeight, List connections) { Map positions = new HashMap<>(); @@ -497,29 +398,22 @@ public abstract class GuiDiagram extends Gui { int posY = compY + rand.nextInt(compHeight); positions.put(id, new Point(posX, posY)); } - double area = compWidth * compHeight; double k = Math.sqrt(area / cluster.size()); int iterations = 100; double temperature = compWidth / 10.0; double cooling = temperature / (iterations + 1); double minDistance = iconSize + slotPadding; - - // Consider only connections within the cluster. List compConnections = new ArrayList<>(); for (DiagramConnection conn : connections) { - if (cluster.contains(conn.idFrom) && cluster.contains(conn.idTo)) { + if (cluster.contains(conn.idFrom) && cluster.contains(conn.idTo)) compConnections.add(conn); - } } - for (int iter = 0; iter < iterations; iter++) { Map disp = new HashMap<>(); for (Integer id : cluster) { - disp.put(id, new double[] {0.0, 0.0}); + disp.put(id, new double[]{0.0, 0.0}); } - - // Repulsive forces. List nodes = new ArrayList<>(cluster); for (int i = 0; i < nodes.size(); i++) { Integer v = nodes.get(i); @@ -527,8 +421,7 @@ public abstract class GuiDiagram extends Gui { for (int j = i + 1; j < nodes.size(); j++) { Integer u = nodes.get(j); Point posU = positions.get(u); - double dx = posV.x - posU.x; - double dy = posV.y - posU.y; + double dx = posV.x - posU.x, dy = posV.y - posU.y; double distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance) distance = minDistance; double repForce = (k * k) / distance; @@ -540,13 +433,10 @@ public abstract class GuiDiagram extends Gui { dispU[1] -= (dy / distance) * repForce; } } - - // Attractive forces. for (DiagramConnection conn : compConnections) { Point posV = positions.get(conn.idFrom); Point posU = positions.get(conn.idTo); - double dx = posV.x - posU.x; - double dy = posV.y - posU.y; + double dx = posV.x - posU.x, dy = posV.y - posU.y; double distance = Math.sqrt(dx * dx + dy * dy); if (distance < 0.01) distance = 0.01; double attrForce = (distance * distance) / k; @@ -557,8 +447,6 @@ public abstract class GuiDiagram extends Gui { dispU[0] += (dx / distance) * attrForce; dispU[1] += (dy / distance) * attrForce; } - - // Update positions. for (Integer id : cluster) { double[] d = disp.get(id); double dispLength = Math.sqrt(d[0] * d[0] + d[1] * d[1]); @@ -570,17 +458,214 @@ public abstract class GuiDiagram extends Gui { positions.get(id).x = newX; positions.get(id).y = newY; } - temperature -= cooling; } - return positions; } + // Manual Layouts – using index (ring) and priority. + private Map calculateCircularManualPositions() { + List icons = getIcons(); + Map> groups = new TreeMap<>(); + for (DiagramIcon icon : icons) { + groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon); + } + Map positions = new HashMap<>(); + int centerX = x + width / 2, centerY = y + height / 2; + int maxRing = groups.isEmpty() ? 0 : Collections.max(groups.keySet()); + int maxRadius = Math.min(width, height) / 2 - iconSize - 10; + double ringSpacing = (maxRing + 1) > 0 ? (double) maxRadius / (maxRing + 1) : 0; + // For each ring, sort by priority. For ring 0, start at -90° (top); for higher rings, start at -135°. + for (Map.Entry> entry : groups.entrySet()) { + int ring = entry.getKey(); + List groupIcons = entry.getValue(); + groupIcons.sort(Comparator.comparingInt(icon -> icon.priority)); + double radius = ringSpacing * (ring + 1); + int count = groupIcons.size(); + double startAngle = (ring == 0) ? -Math.PI / 2 : -3 * Math.PI / 4; + for (int i = 0; i < count; i++) { + double angle = startAngle + 2 * Math.PI * i / count; + int posX = centerX + (int) (radius * Math.cos(angle)); + int posY = centerY + (int) (radius * Math.sin(angle)); + positions.put(groupIcons.get(i).id, new Point(posX, posY)); + } + } + return positions; + } - // --------------- - // Standard Icon Box Drawing (Background + Border) - // --------------- + private Map calculateSquareManualPositions() { + List icons = getIcons(); + Map> groups = new TreeMap<>(); + for (DiagramIcon icon : icons) { + groups.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon); + } + Map positions = new HashMap<>(); + int centerX = x + width / 2, centerY = y + height / 2; + int maxRing = groups.isEmpty() ? 0 : Collections.max(groups.keySet()); + int halfSide = Math.min(width, height) / 2 - iconSize - 10; + double ringSpacing = (maxRing + 1) > 0 ? (double) halfSide / (maxRing + 1) : 0; + // Distribute each ring's icons along the square perimeter. + for (Map.Entry> entry : groups.entrySet()) { + int ring = entry.getKey(); + List groupIcons = entry.getValue(); + groupIcons.sort(Comparator.comparingInt(icon -> icon.priority)); + double offset = ringSpacing * (ring + 1); + int count = groupIcons.size(); + double perimeter = 8 * offset; + for (int i = 0; i < count; i++) { + double posAlong = (perimeter * i) / count; + int posX, posY; + if (posAlong < 2 * offset) { + posX = centerX - (int) offset + (int) posAlong; + posY = centerY - (int) offset; + } else if (posAlong < 4 * offset) { + posX = centerX + (int) offset; + posY = centerY - (int) offset + (int) (posAlong - 2 * offset); + } else if (posAlong < 6 * offset) { + posX = centerX + (int) offset - (int) (posAlong - 4 * offset); + posY = centerY + (int) offset; + } else { + posX = centerX - (int) offset; + posY = centerY + (int) offset - (int) (posAlong - 6 * offset); + } + positions.put(groupIcons.get(i).id, new Point(posX, posY)); + } + } + return positions; + } + + private Map calculateTreeManualPositions() { + List icons = getIcons(); + Map> levels = new TreeMap<>(); + for (DiagramIcon icon : icons) { + levels.computeIfAbsent(icon.index, k -> new ArrayList<>()).add(icon); + } + Map positions = new HashMap<>(); + int maxLevel = levels.isEmpty() ? 0 : Collections.max(levels.keySet()); + int levelHeight = height / (maxLevel + 2); + for (Map.Entry> entry : levels.entrySet()) { + int level = entry.getKey(); + List levelIcons = entry.getValue(); + levelIcons.sort(Comparator.comparingInt(icon -> icon.priority)); + int count = levelIcons.size(); + int horizontalSpacing = (width - 40) / (count + 1); + for (int i = 0; i < count; i++) { + int posX = x + 20 + (i + 1) * horizontalSpacing; + int posY = y + levelHeight * (level + 1); + positions.put(levelIcons.get(i).id, new Point(posX, posY)); + } + } + return positions; + } + + // Arrow Drawing: + /** + * Draws a connection line. + * If curvedArrows is false, draws a straight line. + * Otherwise draws a quadratic Bezier curve using curveAngle for offset. + */ + protected void drawConnectionLine(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) { + int color = getConnectionColor(conn); + if (!useColorScaling) color = 0xFFFFFFFF; + if (!curvedArrows) { + GL11.glPushAttrib(GL11.GL_ENABLE_BIT); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glLineWidth(2.0F); + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = (color & 0xFF) / 255f; + if (dim) { r *= 0.4f; g *= 0.4f; b *= 0.4f; } + GL11.glColor4f(r, g, b, 1f); + GL11.glBegin(GL11.GL_LINES); + GL11.glVertex2i(x1, y1); + GL11.glVertex2i(x2, y2); + GL11.glEnd(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } else { + Point start = new Point(x1, y1); + Point end = new Point(x2, y2); + double dx = x2 - x1, dy = y2 - y1; + double len = Math.sqrt(dx * dx + dy * dy); + if (len == 0) len = 1; + double angleRad = Math.toRadians(curveAngle); + double offset = len * Math.tan(angleRad) / 2; + double perpX = -dy / len, perpY = dx / len; + Point control = new Point((x1 + x2) / 2 + (int)(perpX * offset), + (y1 + y2) / 2 + (int)(perpY * offset)); + int segments = 300; + GL11.glPushAttrib(GL11.GL_ENABLE_BIT); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glLineWidth(2.0F); + float rr = ((color >> 16) & 0xFF) / 255f; + float gg = ((color >> 8) & 0xFF) / 255f; + float bb = (color & 0xFF) / 255f; + if (dim) { rr *= 0.4f; gg *= 0.4f; bb *= 0.4f; } + GL11.glColor4f(rr, gg, bb, 1f); + GL11.glBegin(GL11.GL_LINE_STRIP); + for (int i = 0; i <= segments; i++) { + double t = (double)i / segments; + int bx = (int)((1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * control.x + t * t * end.x); + int by = (int)((1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * control.y + t * t * end.y); + GL11.glVertex2i(bx, by); + } + GL11.glEnd(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } + } + + /** + * Draws the arrow head. + * For curved arrows, the head is aligned with the derivative at t=0.95. + */ + protected void drawArrowHead(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) { + int color = getConnectionColor(conn); + if (!useColorScaling) color = 0xFFFFFFFF; + double angle; + if (!curvedArrows) { + angle = Math.atan2(y2 - y1, x2 - x1); + } else { + double dx = x2 - x1, dy = y2 - y1; + double len = Math.sqrt(dx * dx + dy * dy); + if (len == 0) len = 1; + double offset = len * Math.tan(Math.toRadians(curveAngle)) / 2; + double perpX = -dy / len, perpY = dx / len; + int cx = (x1 + x2) / 2 + (int)(perpX * offset); + int cy = (y1 + y2) / 2 + (int)(perpY * offset); + double t = 0.95; + double bx = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * cx + t * t * x2; + double by = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * cy + t * t * y2; + double dBx = 2 * (1 - t) * (cx - x1) + 2 * t * (x2 - cx); + double dBy = 2 * (1 - t) * (cy - y1) + 2 * t * (y2 - cy); + angle = Math.atan2(dBy, dBx); + } + float defenderEdgeX = x2 - (slotSize / 2f) * (float)Math.cos(angle); + float defenderEdgeY = y2 - (slotSize / 2f) * (float)Math.sin(angle); + int arrowSize = 6; + float leftX = (float)(defenderEdgeX - arrowSize * Math.cos(angle - Math.PI/6)); + float leftY = (float)(defenderEdgeY - arrowSize * Math.sin(angle - Math.PI/6)); + float rightX = (float)(defenderEdgeX - arrowSize * Math.cos(angle + Math.PI/6)); + float rightY = (float)(defenderEdgeY - arrowSize * Math.sin(angle + Math.PI/6)); + GL11.glPushAttrib(GL11.GL_ENABLE_BIT); + GL11.glDisable(GL11.GL_TEXTURE_2D); + float rr = ((color >> 16)&0xFF)/255f; + float gg = ((color >> 8)&0xFF)/255f; + float bb = (color&0xFF)/255f; + if(dim){ rr*=0.4f; gg*=0.4f; bb*=0.4f; } + GL11.glColor4f(rr, gg, bb, 1f); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glBegin(GL11.GL_TRIANGLES); + GL11.glVertex2f(defenderEdgeX, defenderEdgeY); + GL11.glVertex2f(leftX, leftY); + GL11.glVertex2f(rightX, rightY); + GL11.glEnd(); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } + + // Standard Icon Box Drawing protected void drawIconBox(int slotX, int slotY, int slotSize, boolean highlighted) { int bg = highlighted ? mixColors(iconBackgroundColor, 0xFFFFFFFF, 0.2f) : iconBackgroundColor; drawRect(slotX, slotY, slotX + slotSize, slotY + slotSize, bg); @@ -591,7 +676,6 @@ public abstract class GuiDiagram extends Gui { drawVerticalLine(slotX + slotSize - 1 - i, slotY + i, slotY + slotSize - i, iconBorderColor); } } - protected int mixColors(int color1, int color2, float ratio) { int a1 = (color1 >> 24) & 0xFF, r1 = (color1 >> 16) & 0xFF, g1 = (color1 >> 8) & 0xFF, b1 = color1 & 0xFF; int a2 = (color2 >> 24) & 0xFF, r2 = (color2 >> 16) & 0xFF, g2 = (color2 >> 8) & 0xFF, b2 = color2 & 0xFF; @@ -602,39 +686,57 @@ public abstract class GuiDiagram extends Gui { return (a << 24) | (r << 16) | (g << 8) | b; } - // --------------- - // Main Draw Method - // --------------- - public void drawDiagram(int mouseX, int mouseY, boolean subGui) { - // Only allow mouse interaction if parent's not showing a subgui. - boolean allowInput = (parent == null || !parent.hasSubGui()); - // Update zoom based on mouse scroll if allowed. - if (allowInput && isWithin(mouseX, mouseY) && !subGui) { - handleMouseScroll(Mouse.getDWheel()); + // Hit-detection helper for curved arrows. + private double distanceToBezier(Point start, Point control, Point end, int segments, int px, int py) { + double minDist = Double.MAX_VALUE; + Point prev = null; + for (int i = 0; i <= segments; i++) { + double t = (double)i / segments; + int x = (int)((1 - t) * (1 - t) * start.x + 2 * (1 - t) * t * control.x + t * t * end.x); + int y = (int)((1 - t) * (1 - t) * start.y + 2 * (1 - t) * t * control.y + t * t * end.y); + Point curr = new Point(x, y); + if (prev != null) { + double d = distancePointToSegment(px, py, prev, curr); + if (d < minDist) minDist = d; + } + prev = curr; } + return minDist; + } + private double distancePointToSegment(int px, int py, Point a, Point b) { + double A = px - a.x, B = py - a.y; + double C = b.x - a.x, D = b.y - a.y; + double dot = A * C + B * D; + double lenSq = C * C + D * D; + double param = (lenSq != 0) ? (dot / lenSq) : -1; + double xx, yy; + if (param < 0) { xx = a.x; yy = a.y; } + else if (param > 1) { xx = b.x; yy = b.y; } + else { xx = a.x + param * C; yy = a.y + param * D; } + double dx = px - xx, dy = py - yy; + return Math.sqrt(dx * dx + dy * dy); + } + // Main Draw Method + public void drawDiagram(int mouseX, int mouseY, boolean subGui) { + boolean allowInput = (parent == null || !parent.hasSubGui()); + if (allowInput && isWithin(mouseX, mouseY) && !subGui) + handleMouseScroll(Mouse.getDWheel()); Minecraft mc = Minecraft.getMinecraft(); ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); int factor = sr.getScaleFactor(); - Map positions = calculatePositions(); - int centerX = x + width / 2; - int centerY = y + height / 2; - + int centerX = x + width / 2, centerY = y + height / 2; int effectiveMouseX = (int)(((float)mouseX - (centerX + panX)) / zoom + centerX); int effectiveMouseY = (int)(((float)mouseY - (centerY + panY)) / zoom + centerY); - Integer hoveredIconId = null; int hoveredConnFrom = -1, hoveredConnTo = -1; Set selectedIconIds = new HashSet<>(); - - // Hit testing for icons (only consider enabled icons) for (DiagramIcon icon : getIcons()) { if (!icon.enabled) continue; Point pos = positions.get(icon.id); if (pos == null) continue; - int slotX = pos.x - slotSize / 2; - int slotY = pos.y - slotSize / 2; + int slotX = pos.x - slotSize / 2, slotY = pos.y - slotSize / 2; if (effectiveMouseX >= slotX && effectiveMouseX < slotX + slotSize && effectiveMouseY >= slotY && effectiveMouseY < slotY + slotSize) { hoveredIconId = icon.id; @@ -644,7 +746,6 @@ public abstract class GuiDiagram extends Gui { } if (hoveredIconId != null) { for (DiagramConnection conn : getConnections()) { - // Validate connection: both endpoints must exist and be enabled. DiagramIcon iconFrom = getIconById(conn.idFrom); DiagramIcon iconTo = getIconById(conn.idTo); if (iconFrom == null || iconTo == null || !iconFrom.enabled || !iconTo.enabled) @@ -665,7 +766,20 @@ public abstract class GuiDiagram extends Gui { Point pFrom = positions.get(conn.idFrom); Point pTo = positions.get(conn.idTo); if (pFrom == null || pTo == null) continue; - double dist = pointLineDistance(effectiveMouseX, effectiveMouseY, pFrom.x, pFrom.y, pTo.x, pTo.y); + double dist; + if (curvedArrows) { + double dx = pTo.x - pFrom.x, dy = pTo.y - pFrom.y; + double len = Math.sqrt(dx * dx + dy * dy); + if (len == 0) len = 1; + double angleRad = Math.toRadians(curveAngle); + double offset = len * Math.tan(angleRad) / 2; + double perpX = -dy / len, perpY = dx / len; + Point control = new Point((pFrom.x+pTo.x)/2 + (int)(perpX*offset), + (pFrom.y+pTo.y)/2 + (int)(perpY*offset)); + dist = distanceToBezier(pFrom, control, pTo, 20, effectiveMouseX, effectiveMouseY); + } else { + dist = pointLineDistance(effectiveMouseX, effectiveMouseY, pFrom.x, pFrom.y, pTo.x, pTo.y); + } if (dist < threshold) { hoveredConnFrom = conn.idFrom; hoveredConnTo = conn.idTo; @@ -675,17 +789,13 @@ public abstract class GuiDiagram extends Gui { } } } - GL11.glEnable(GL11.GL_SCISSOR_TEST); GL11.glScissor(x * factor, (sr.getScaledHeight() - (y + height)) * factor, width * factor, height * factor); drawRect(x, y, x + width, y + height, 0xFF333333); - GL11.glPushMatrix(); GL11.glTranslatef(centerX + panX, centerY + panY, 0); GL11.glScalef(zoom, zoom, 1.0f); GL11.glTranslatef(-centerX, -centerY, 0); - - // Draw connections (only for endpoints that are enabled). for (DiagramConnection conn : getConnections()) { DiagramIcon iconFrom = getIconById(conn.idFrom); DiagramIcon iconTo = getIconById(conn.idTo); @@ -694,30 +804,21 @@ public abstract class GuiDiagram extends Gui { Point pFrom = positions.get(conn.idFrom); Point pTo = positions.get(conn.idTo); if (pFrom == null || pTo == null) continue; - int ax = pFrom.x; - int ay = pFrom.y; - int bx = pTo.x; - int by = pTo.y; - boolean dim = !selectedIconIds.isEmpty() && - !(selectedIconIds.contains(conn.idFrom) && selectedIconIds.contains(conn.idTo)); + int ax = pFrom.x, ay = pFrom.y, bx = pTo.x, by = pTo.y; + boolean dim = !selectedIconIds.isEmpty() && !(selectedIconIds.contains(conn.idFrom) && selectedIconIds.contains(conn.idTo)); drawConnectionLine(ax, ay, bx, by, conn, dim); } - - // Draw icons. for (DiagramIcon icon : getIcons()) { if (!icon.enabled) continue; Point pos = positions.get(icon.id); if (pos == null) continue; - int slotX = pos.x - slotSize / 2; - int slotY = pos.y - slotSize / 2; + int slotX = pos.x - slotSize / 2, slotY = pos.y - slotSize / 2; boolean highlighted = selectedIconIds.isEmpty() || selectedIconIds.contains(icon.id); drawIconBox(slotX, slotY, slotSize, highlighted); IconRenderState state = highlighted ? IconRenderState.HIGHLIGHTED : IconRenderState.NOT_HIGHLIGHTED; renderIcon(icon, pos.x, pos.y, state); } GL11.glPopMatrix(); - - // Draw arrow heads. if (showArrowHeads) { GL11.glPushMatrix(); GL11.glTranslatef(centerX + panX, centerY + panY, 0); @@ -731,161 +832,86 @@ public abstract class GuiDiagram extends Gui { Point pFrom = positions.get(conn.idFrom); Point pTo = positions.get(conn.idTo); if (pFrom == null || pTo == null) continue; - int ax = pFrom.x; - int ay = pFrom.y; - int bx = pTo.x; - int by = pTo.y; - boolean dim = !selectedIconIds.isEmpty() && - !(selectedIconIds.contains(conn.idFrom) && selectedIconIds.contains(conn.idTo)); + int ax = pFrom.x, ay = pFrom.y, bx = pTo.x, by = pTo.y; + boolean dim = !selectedIconIds.isEmpty() && !(selectedIconIds.contains(conn.idFrom) && selectedIconIds.contains(conn.idTo)); drawArrowHead(ax, ay, bx, by, conn, dim); } GL11.glPopMatrix(); } GL11.glDisable(GL11.GL_SCISSOR_TEST); - - // Tooltips. if (hoveredIconId != null) { DiagramIcon icon = getIconById(hoveredIconId); List tooltip = getIconTooltip(icon); - if (tooltip != null && !tooltip.isEmpty()) { + if (tooltip != null && !tooltip.isEmpty()) drawHoveringText(tooltip, mouseX, mouseY, mc.fontRenderer); - } } else if (hoveredConnFrom != -1 && hoveredConnTo != -1) { DiagramConnection conn = getConnectionByIds(hoveredConnFrom, hoveredConnTo); List tooltip = getConnectionTooltip(conn); - if (tooltip != null && !tooltip.isEmpty()) { + if (tooltip != null && !tooltip.isEmpty()) drawHoveringText(tooltip, mouseX, mouseY, mc.fontRenderer); - } } } protected DiagramIcon getIconById(int id) { for (DiagramIcon icon : getIcons()) { - if (icon.id == id) - return icon; + if (icon.id == id) return icon; } return null; } - protected DiagramConnection getConnectionByIds(int idFrom, int idTo) { for (DiagramConnection conn : getConnections()) { - if (conn.idFrom == idFrom && conn.idTo == idTo) - return conn; + if (conn.idFrom == idFrom && conn.idTo == idTo) return conn; } return null; } - - protected void drawConnectionLine(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) { - int color = getConnectionColor(conn); - if (!useColorScaling) { color = 0xFFFFFFFF; } - GL11.glPushAttrib(GL11.GL_ENABLE_BIT); - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glLineWidth(2.0F); - float r = ((color >> 16)&0xFF)/255f; - float g = ((color >> 8)&0xFF)/255f; - float b = (color&0xFF)/255f; - if (dim) { r*=0.4f; g*=0.4f; b*=0.4f; } - GL11.glColor4f(r, g, b, 1f); - GL11.glBegin(GL11.GL_LINES); - GL11.glVertex2f(x1, y1); - GL11.glVertex2f(x2, y2); - GL11.glEnd(); - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glPopAttrib(); - } - - protected void drawArrowHead(int x1, int y1, int x2, int y2, DiagramConnection conn, boolean dim) { - float dx = x2 - x1, dy = y2 - y1; - float len = (float)Math.sqrt(dx*dx+dy*dy); - if (len==0)return; - float ux = dx/len, uy = dy/len; - float defenderEdgeX = x2 - ux*(slotSize/2f); - float defenderEdgeY = y2 - uy*(slotSize/2f); - int arrowSize = 6; - double angle = Math.atan2(uy,ux); - float leftX = (float)(defenderEdgeX - arrowSize * Math.cos(angle - Math.PI/6)); - float leftY = (float)(defenderEdgeY - arrowSize * Math.sin(angle - Math.PI/6)); - float rightX = (float)(defenderEdgeX - arrowSize * Math.cos(angle + Math.PI/6)); - float rightY = (float)(defenderEdgeY - arrowSize * Math.sin(angle + Math.PI/6)); - int color = getConnectionColor(conn); - if (!useColorScaling) { color = 0xFFFFFFFF; } - GL11.glPushAttrib(GL11.GL_ENABLE_BIT); - GL11.glDisable(GL11.GL_TEXTURE_2D); - float r = ((color >> 16)&0xFF)/255f; - float g = ((color >> 8)&0xFF)/255f; - float b = (color&0xFF)/255f; - if(dim){ r*=0.4f; g*=0.4f; b*=0.4f; } - GL11.glColor4f(r, g, b, 1f); - GL11.glDisable(GL11.GL_CULL_FACE); - GL11.glBegin(GL11.GL_TRIANGLES); - GL11.glVertex2f(defenderEdgeX, defenderEdgeY); - GL11.glVertex2f(leftX, leftY); - GL11.glVertex2f(rightX, rightY); - GL11.glEnd(); - GL11.glEnable(GL11.GL_CULL_FACE); - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glPopAttrib(); - } - protected int getConnectionColor(DiagramConnection conn) { float percent = conn.percent; - if (percent<=0.10f)return 0xFF80FF80; - else if (percent<=0.40f)return 0xFFCCFF66; - else if (percent<=0.50f)return 0xFFFFA500; + if (percent <= 0.10f) return 0xFF80FF80; + else if (percent <= 0.40f) return 0xFFCCFF66; + else if (percent <= 0.50f) return 0xFFFFA500; else { - float ratio = (percent-0.50f)/0.50f; + float ratio = (percent - 0.50f) / 0.50f; ratio = Math.min(1f, Math.max(0f, ratio)); int r1 = 0xFF, g1 = 0xA5, b1 = 0x00; int r2 = 0xFF, g2 = 0x00, b2 = 0x00; - int r = r1 + (int)((r2-r1)*ratio); - int g = g1 + (int)((g2-g1)*ratio); - int b = b1 + (int)((b2-b1)*ratio); + int r = r1 + (int)((r2 - r1) * ratio); + int g = g1 + (int)((g2 - g1) * ratio); + int b = b1 + (int)((b2 - b1) * ratio); return (0xFF << 24) | (r << 16) | (g << 8) | b; } } - - private double pointLineDistance(double px, double py, double x1, double y1, double x2, double y2) { + private double pointLineDistance(int px, int py, int x1, int y1, int x2, int y2) { double A = px - x1, B = py - y1, C = x2 - x1, D = y2 - y1; double dot = A * C + B * D; double lenSq = C * C + D * D; - double param = (lenSq!=0)?(dot/lenSq):-1; + double param = (lenSq != 0) ? (dot / lenSq) : -1; double xx, yy; - if(param<0){ xx = x1; yy = y1; } - else if(param>1){ xx = x2; yy = y2; } - else { xx = x1+param*C; yy = y1+param*D; } - double dx = px-xx, dy = py-yy; - return Math.sqrt(dx*dx+dy*dy); + if (param < 0) { xx = x1; yy = y1; } + else if (param > 1) { xx = x2; yy = y2; } + else { xx = x1 + param * C; yy = y1 + param * D; } + double dx = px - xx, dy = py - yy; + return Math.sqrt(dx * dx + dy * dy); } - // --------------- // Mouse Handling - // --------------- public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) { - // Do not process if parent's subgui is active. if (parent != null && parent.hasSubGui()) return false; - // If left mouse button: if (mouseButton == 0) { - // First, try to dispatch icon events for pressable icons. for (DiagramIcon icon : getIcons()) { - if (!icon.enabled || !icon.pressable) - continue; + if (!icon.enabled || !icon.pressable) continue; Point pos = calculatePositions().get(icon.id); if (pos == null) continue; - int centerX = x + width / 2; - int centerY = y + height / 2; - int effectiveX = (int)(((float)mouseX - (centerX + panX)) / zoom + centerX); - int effectiveY = (int)(((float)mouseY - (centerY + panY)) / zoom + centerY); - int slotX = pos.x - slotSize / 2; - int slotY = pos.y - slotSize / 2; - if (effectiveX >= slotX && effectiveX < slotX + slotSize && - effectiveY >= slotY && effectiveY < slotY + slotSize) { + int centerX = x + width/2, centerY = y + height/2; + int effectiveX = (int)(((float)mouseX - (centerX+panX))/zoom+centerX); + int effectiveY = (int)(((float)mouseY - (centerY+panY))/zoom+centerY); + int slotX = pos.x - slotSize/2, slotY = pos.y - slotSize/2; + if (effectiveX >= slotX && effectiveX < slotX+slotSize && + effectiveY >= slotY && effectiveY < slotY+slotSize) { onIconClick(icon); - // Mark as pressed for held/release events. currentlyPressedIcon = icon; return true; } } - // Otherwise, start dragging for panning. if (isWithin(mouseX, mouseY)) { dragging = true; lastDragX = mouseX; @@ -895,25 +921,18 @@ public abstract class GuiDiagram extends Gui { } return false; } - protected DiagramIcon currentlyPressedIcon = null; - public void mouseClickMove(int mouseX, int mouseY, int mouseButton, long timeSinceLastClick) { if (parent != null && parent.hasSubGui()) return; if (dragging) { - int dx = mouseX - lastDragX; - int dy = mouseY - lastDragY; - panX += dx / zoom * 0.7f; - panY += dy / zoom * 0.7f; + int dx = mouseX - lastDragX, dy = mouseY - lastDragY; + panX += dx/zoom*0.7f; + panY += dy/zoom*0.7f; lastDragX = mouseX; lastDragY = mouseY; } - // If an icon is pressed, dispatch held event. - if (currentlyPressedIcon != null) { - onIconHeld(currentlyPressedIcon); - } + if (currentlyPressedIcon != null) onIconHeld(currentlyPressedIcon); } - public void mouseReleased(int mouseX, int mouseY, int state) { if (parent != null && parent.hasSubGui()) return; if (currentlyPressedIcon != null) { @@ -922,58 +941,51 @@ public abstract class GuiDiagram extends Gui { } dragging = false; } - public boolean isWithin(int mouseX, int mouseY) { - if(parent.hasSubGui()) + if (parent.hasSubGui()) return false; return mouseX >= x && mouseX <= x+width && mouseY >= y && mouseY <= y+height; } - /** - * Handles zoom in/out based on mouse wheel scrolling. + * Handles zoom based on mouse wheel scrolling. */ public void handleMouseScroll(int scrollDelta) { zoom += scrollDelta * 0.0009f; - if(zoom < 0.5f) zoom = 0.5f; - if(zoom > 2.0f) zoom = 2.0f; + if (zoom < 0.5f) zoom = 0.5f; + if (zoom > 2.0f) zoom = 2.0f; } - /** - * Override this method if you wish to render tooltips differently. + * Override to render tooltips. */ protected void drawHoveringText(List text, int mouseX, int mouseY, FontRenderer fontRenderer) { // Default implementation is empty. } - - // --------------- // Flag Setters - // --------------- public void setShowArrowHeads(boolean showArrowHeads) { this.showArrowHeads = showArrowHeads; } - public void setUseColorScaling(boolean useColorScaling) { this.useColorScaling = useColorScaling; } - public enum IconRenderState { DEFAULT, HIGHLIGHTED, NOT_HIGHLIGHTED; } - - // --------------- - // Inner Classes for Data Wrapping - // --------------- + // Inner Classes public static class DiagramIcon { public int id; - public boolean enabled = true; // If false, the icon is not rendered. - public boolean pressable = false; // If true, it will receive mouse events. - public DiagramIcon(int id) { + public boolean enabled = true; + public boolean pressable = false; + public int index = 0; // For row, ring, or level. + public int priority = 0; // For ordering. + public DiagramIcon(int id) { this.id = id; } + public DiagramIcon(int id, int index, int priority) { this.id = id; + this.index = index; + this.priority = priority; } } - public static class DiagramConnection { public int idFrom, idTo; public float percent; @@ -985,24 +997,14 @@ public abstract class GuiDiagram extends Gui { this.hoverText = hoverText; } } - - // --------------- - // Icon Event Callbacks (override these in subclasses as needed) - // --------------- + // Icon Event Callbacks protected void onIconClick(DiagramIcon icon) { - // Called when a pressable icon is clicked. System.out.println("Icon " + icon.id + " clicked."); } - protected void onIconHeld(DiagramIcon icon) { - // Called repeatedly if an icon remains pressed. - // (You can add timing logic to fire this only after a threshold.) - // For now, we simply print once. System.out.println("Icon " + icon.id + " held."); } - protected void onIconRelease(DiagramIcon icon) { - // Called when a pressable icon is released. System.out.println("Icon " + icon.id + " released."); } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiMagicMap.java b/src/main/java/noppes/npcs/client/gui/global/GuiMagicMap.java index da8ac8217..7954d5f56 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiMagicMap.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiMagicMap.java @@ -31,7 +31,9 @@ public class GuiMagicMap extends GuiDiagram { for (Magic magic : MagicController.getInstance().magics.values()) { magics.add(magic); } - setLayout(DiagramLayout.GENERATED); + setLayout(DiagramLayout.CIRCULAR_MANUAL); + this.setCurvedArrows(true); + this.setCurveAngle(25); } @Override @@ -121,7 +123,7 @@ public class GuiMagicMap extends GuiDiagram { private class MagicIcon extends DiagramIcon { public Magic magic; public MagicIcon(Magic magic) { - super(magic.id); + super(magic.id, magic.index, magic.priority); this.magic = magic; } } diff --git a/src/main/java/noppes/npcs/controllers/MagicController.java b/src/main/java/noppes/npcs/controllers/MagicController.java index 9aafa1820..b2c6385af 100644 --- a/src/main/java/noppes/npcs/controllers/MagicController.java +++ b/src/main/java/noppes/npcs/controllers/MagicController.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers; +import net.minecraft.init.Blocks; import net.minecraft.item.ItemStack; import net.minecraft.nbt.CompressedStreamTools; import net.minecraft.nbt.NBTTagCompound; @@ -55,31 +56,179 @@ public class MagicController { } } catch (Exception ee) { } } + magics.clear(); + if (false) { + // Create default Pokémon types + Magic normal = new Magic(0, "Normal", 0xA8A878); + normal.setIconItem(new ItemStack(net.minecraft.init.Items.paper)); // using paper for Normal - if (magics.isEmpty()) { + Magic fire = new Magic(1, "Fire", 0xF08030); + fire.setIconItem(new ItemStack(net.minecraft.init.Items.flint)); // flint for Fire + + Magic water = new Magic(2, "Water", 0x6890F0); + water.setIconItem(new ItemStack(net.minecraft.init.Items.water_bucket)); // water bucket + + Magic electric = new Magic(3, "Electric", 0xF8D030); + electric.setIconItem(new ItemStack(net.minecraft.init.Items.redstone)); // redstone + + Magic grass = new Magic(4, "Grass", 0x78C850); + grass.setIconItem(new ItemStack(net.minecraft.init.Items.wheat_seeds)); // wheat seeds + + Magic ice = new Magic(5, "Ice", 0x98D8D8); + ice.setIconItem(new ItemStack(net.minecraft.init.Items.snowball)); // snowball for Ice + + Magic fighting = new Magic(6, "Fighting", 0xC03028); + fighting.setIconItem(new ItemStack(net.minecraft.init.Items.iron_sword)); // sword for Fighting + + Magic poison = new Magic(7, "Poison", 0xA040A0); + poison.setIconItem(new ItemStack(net.minecraft.init.Items.potionitem)); // potion item for Poison + + Magic ground = new Magic(8, "Ground", 0xE0C068); + ground.setIconItem(new ItemStack(Blocks.dirt)); // dirt for Ground + + Magic flying = new Magic(9, "Flying", 0xA890F0); + flying.setIconItem(new ItemStack(net.minecraft.init.Items.feather)); // feather for Flying + + Magic psychic = new Magic(10, "Psychic", 0xF85888); + psychic.setIconItem(new ItemStack(net.minecraft.init.Items.ender_pearl)); // ender pearl for Psychic + + Magic bug = new Magic(11, "Bug", 0xA8B820); + bug.setIconItem(new ItemStack(net.minecraft.init.Items.spider_eye)); // spider eye for Bug + + Magic rock = new Magic(12, "Rock", 0xB8A038); + rock.setIconItem(new ItemStack(Blocks.cobblestone)); // cobblestone for Rock + + Magic ghost = new Magic(13, "Ghost", 0x705898); + ghost.setIconItem(new ItemStack(net.minecraft.init.Items.skull)); // skull for Ghost + + Magic dragon = new Magic(14, "Dragon", 0x7038F8); + dragon.setIconItem(new ItemStack(Blocks.dragon_egg)); // dragon egg + + Magic dark = new Magic(15, "Dark", 0x705848); + dark.setIconItem(new ItemStack(net.minecraft.init.Items.coal)); // coal for Dark + + Magic steel = new Magic(16, "Steel", 0xB8B8D0); + steel.setIconItem(new ItemStack(net.minecraft.init.Items.iron_ingot)); // iron ingot for Steel + + Magic fairy = new Magic(17, "Fairy", 0xEE99AC); + fairy.setIconItem(new ItemStack(net.minecraft.init.Items.sugar)); // sugar for Fairy + + // Now set weaknesses (using a multiplier of 2.0f to indicate “double damage”). + // The following is based on a common Pokémon type chart. + normal.weaknesses.put(fighting.id, 2.0f); + + fire.weaknesses.put(water.id, 2.0f); + fire.weaknesses.put(ground.id, 2.0f); + fire.weaknesses.put(rock.id, 2.0f); + + water.weaknesses.put(electric.id, 2.0f); + water.weaknesses.put(grass.id, 2.0f); + + electric.weaknesses.put(ground.id, 2.0f); + + grass.weaknesses.put(fire.id, 2.0f); + grass.weaknesses.put(ice.id, 2.0f); + grass.weaknesses.put(poison.id, 2.0f); + grass.weaknesses.put(flying.id, 2.0f); + grass.weaknesses.put(bug.id, 2.0f); + + ice.weaknesses.put(fire.id, 2.0f); + ice.weaknesses.put(fighting.id, 2.0f); + ice.weaknesses.put(rock.id, 2.0f); + ice.weaknesses.put(steel.id, 2.0f); + + fighting.weaknesses.put(flying.id, 2.0f); + fighting.weaknesses.put(psychic.id, 2.0f); + fighting.weaknesses.put(fairy.id, 2.0f); + + poison.weaknesses.put(ground.id, 2.0f); + poison.weaknesses.put(psychic.id, 2.0f); + + ground.weaknesses.put(water.id, 2.0f); + ground.weaknesses.put(grass.id, 2.0f); + ground.weaknesses.put(ice.id, 2.0f); + + flying.weaknesses.put(electric.id, 2.0f); + flying.weaknesses.put(ice.id, 2.0f); + flying.weaknesses.put(rock.id, 2.0f); + + psychic.weaknesses.put(bug.id, 2.0f); + psychic.weaknesses.put(ghost.id, 2.0f); + psychic.weaknesses.put(dark.id, 2.0f); + + bug.weaknesses.put(fire.id, 2.0f); + bug.weaknesses.put(flying.id, 2.0f); + bug.weaknesses.put(rock.id, 2.0f); + + rock.weaknesses.put(water.id, 2.0f); + rock.weaknesses.put(grass.id, 2.0f); + rock.weaknesses.put(fighting.id, 2.0f); + rock.weaknesses.put(ground.id, 2.0f); + rock.weaknesses.put(steel.id, 2.0f); + + ghost.weaknesses.put(ghost.id, 2.0f); + ghost.weaknesses.put(dark.id, 2.0f); + + dragon.weaknesses.put(ice.id, 2.0f); + dragon.weaknesses.put(dragon.id, 2.0f); + dragon.weaknesses.put(fairy.id, 2.0f); + + dark.weaknesses.put(fighting.id, 2.0f); + dark.weaknesses.put(bug.id, 2.0f); + dark.weaknesses.put(fairy.id, 2.0f); + + steel.weaknesses.put(fire.id, 2.0f); + steel.weaknesses.put(fighting.id, 2.0f); + steel.weaknesses.put(ground.id, 2.0f); + + fairy.weaknesses.put(poison.id, 2.0f); + fairy.weaknesses.put(steel.id, 2.0f); + + // Register the types. + magics.put(normal.id, normal); + magics.put(fire.id, fire); + magics.put(water.id, water); + magics.put(electric.id, electric); + magics.put(grass.id, grass); + magics.put(ice.id, ice); + magics.put(fighting.id, fighting); + magics.put(poison.id, poison); + magics.put(ground.id, ground); + magics.put(flying.id, flying); + magics.put(psychic.id, psychic); + magics.put(bug.id, bug); + magics.put(rock.id, rock); + magics.put(ghost.id, ghost); + magics.put(dragon.id, dragon); + magics.put(dark.id, dark); + magics.put(steel.id, steel); + magics.put(fairy.id, fairy); + } + + if (true || magics.isEmpty()) { // Create default magics - Magic earth = new Magic(0, "Earth", 0x00DD00); + Magic earth = new Magic(0, "Earth", 0x00DD00, 0, 0); earth.setIconItem(new ItemStack(CustomItems.earthElement)); - Magic water = new Magic(1, "Water", 0xF2DD00); + Magic water = new Magic(1, "Water", 0xF2DD00, 0, 1); water.setIconItem(new ItemStack(CustomItems.waterElement)); - Magic fire = new Magic(2, "Fire", 0xDD0000); + Magic fire = new Magic(2, "Fire", 0xDD0000, 0, 2); fire.setIconItem(new ItemStack(CustomItems.spellFire)); - Magic air = new Magic(3, "Air", 0xDD0000); + Magic air = new Magic(3, "Air", 0xDD0000, 0, 3); air.setIconItem(new ItemStack(CustomItems.airElement)); - Magic dark = new Magic(4, "Dark", 0xDD0000); + Magic dark = new Magic(4, "Dark", 0xDD0000, 1, 3); dark.setIconItem(new ItemStack(CustomItems.spellDark)); - Magic holy = new Magic(5, "Holy", 0xDD0000); + Magic holy = new Magic(5, "Holy", 0xDD0000, 1, 1); holy.setIconItem(new ItemStack(CustomItems.spellHoly)); - Magic nature = new Magic(6, "Nature", 0xDD0000); + Magic nature = new Magic(6, "Nature", 0xDD0000, 1, 0); nature.setIconItem(new ItemStack(CustomItems.spellNature)); - Magic arcane = new Magic(7, "Arcane", 0xDD0000); + Magic arcane = new Magic(7, "Arcane", 0xDD0000, 1, 2); arcane.setIconItem(new ItemStack(CustomItems.spellArcane)); // Insiders diff --git a/src/main/java/noppes/npcs/controllers/data/Magic.java b/src/main/java/noppes/npcs/controllers/data/Magic.java index eaea2608c..1b5636bc1 100644 --- a/src/main/java/noppes/npcs/controllers/data/Magic.java +++ b/src/main/java/noppes/npcs/controllers/data/Magic.java @@ -14,6 +14,9 @@ public class Magic implements IMagic { public int color = Integer.parseInt("FF00", 16); public int id = -1; + public int index; + public int priority; + // Icon as an ItemStack or as a texture file public ItemStack iconItem = null; public String iconTexture = ""; @@ -30,6 +33,14 @@ public class Magic implements IMagic { this.id = id; } + public Magic(int id, String name, int color, int index, int priority) { + this.name = name; + this.color = color; + this.id = id; + this.index = index; + this.priority = priority; + } + public static String formatName(String name) { name = name.toLowerCase().trim(); return name.substring(0, 1).toUpperCase() + name.substring(1); @@ -39,6 +50,8 @@ public class Magic implements IMagic { name = compound.getString("Name"); color = compound.getInteger("Color"); id = compound.getInteger("Slot"); + index = compound.getInteger("Index"); + priority = compound.getInteger("Priority"); if(compound.hasKey("IconItem")){ NBTTagCompound itemTag = compound.getCompoundTag("IconItem"); @@ -63,6 +76,8 @@ public class Magic implements IMagic { compound.setInteger("Slot", id); compound.setString("Name", name); compound.setInteger("Color", color); + compound.setInteger("Index", index); + compound.setInteger("Priority", priority); if(iconItem != null) { NBTTagCompound itemTag = new NBTTagCompound(); @@ -96,6 +111,22 @@ public class Magic implements IMagic { this.name = name; } + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + public void setColor(int color) { this.color = color; }