From 7948aa20880b550feb16948ae610164a6a7f6103 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 21:06:48 +0200 Subject: [PATCH] feat(io): GUI MapPanel (Graphics2D) in the side panel; drop exits text Co-Authored-By: Claude Opus 4.8 (1M context) --- .../thb/jeanluc/adventure/io/MapPanel.java | 130 ++++++++++++++++++ .../thb/jeanluc/adventure/io/SwingIO.java | 28 ++-- 2 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java new file mode 100644 index 0000000..0b90088 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/MapPanel.java @@ -0,0 +1,130 @@ +package thb.jeanluc.adventure.io; + +import thb.jeanluc.adventure.io.text.CellState; +import thb.jeanluc.adventure.io.text.Connection; +import thb.jeanluc.adventure.io.text.MapView; +import thb.jeanluc.adventure.io.text.RoomCell; + +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Stroke; +import java.util.List; + +/** Draws a {@link MapView} as outlined rooms linked by corridors (Graphics2D). */ +public class MapPanel extends JComponent { + + private static final int CELL_W = 96; + private static final int CELL_H = 46; + private static final int GAP_X = 34; + private static final int GAP_Y = 30; + private static final int PAD = 16; + + private static final Color BG = new Color(0x0e, 0x12, 0x18); + private static final Color CORRIDOR = new Color(0x46, 0x70, 0x8a); + private static final Color VISITED = new Color(0xcf, 0xd6, 0xe0); + private static final Color CURRENT = new Color(0xe6, 0xc3, 0x4a); + private static final Color KNOWN = new Color(0x3a, 0x46, 0x55); + + private final Font font; + private MapView view = new MapView(List.of(), List.of()); + + public MapPanel(Font font) { + this.font = font.deriveFont(11f); + setBackground(BG); + setOpaque(true); + } + + /** Replaces the rendered map and scrolls the current room into view. */ + public void show(MapView v) { + SwingUtilities.invokeLater(() -> { + this.view = v; + int maxX = 0; + int maxY = 0; + for (RoomCell c : v.cells()) { + maxX = Math.max(maxX, c.x()); + maxY = Math.max(maxY, c.y()); + } + setPreferredSize(new Dimension( + PAD * 2 + (maxX + 1) * CELL_W + maxX * GAP_X, + PAD * 2 + (maxY + 1) * CELL_H + maxY * GAP_Y)); + revalidate(); + repaint(); + v.cells().stream().filter(c -> c.state() == CellState.CURRENT).findFirst() + .ifPresent(c -> scrollRectToVisible(boundsExpanded(c))); + }); + } + + private Rectangle boundsExpanded(RoomCell c) { + int x = PAD + c.x() * (CELL_W + GAP_X); + int y = PAD + c.y() * (CELL_H + GAP_Y); + return new Rectangle(x - GAP_X, y - GAP_Y, CELL_W + 2 * GAP_X, CELL_H + 2 * GAP_Y); + } + + private int cx(RoomCell c) { + return PAD + c.x() * (CELL_W + GAP_X); + } + + private int cy(RoomCell c) { + return PAD + c.y() * (CELL_H + GAP_Y); + } + + @Override + protected void paintComponent(Graphics g0) { + Graphics2D g = (Graphics2D) g0.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setColor(BG); + g.fillRect(0, 0, getWidth(), getHeight()); + g.setFont(font); + + Stroke solid = new BasicStroke(2f); + Stroke dashed = new BasicStroke(1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, + 10f, new float[]{4f, 3f}, 0f); + + // corridors first + for (Connection cn : view.connections()) { + boolean known = cn.from().state() == CellState.KNOWN || cn.to().state() == CellState.KNOWN; + g.setStroke(known ? dashed : solid); + g.setColor(known ? KNOWN : CORRIDOR); + int x1 = cx(cn.from()) + CELL_W / 2; + int y1 = cy(cn.from()) + CELL_H / 2; + int x2 = cx(cn.to()) + CELL_W / 2; + int y2 = cy(cn.to()) + CELL_H / 2; + g.drawLine(x1, y1, x2, y2); + } + + // rooms on top + for (RoomCell c : view.cells()) { + int x = cx(c); + int y = cy(c); + Color stroke = switch (c.state()) { + case CURRENT -> CURRENT; + case VISITED -> VISITED; + case KNOWN -> KNOWN; + }; + g.setColor(c.state() == CellState.CURRENT + ? new Color(0x27, 0x21, 0x10) + : new Color(0x11, 0x16, 0x1f)); + g.fillRoundRect(x, y, CELL_W, CELL_H, 12, 12); + + g.setStroke(c.state() == CellState.KNOWN + ? dashed + : new BasicStroke(c.state() == CellState.CURRENT ? 2.5f : 1.5f)); + g.setColor(stroke); + g.drawRoundRect(x, y, CELL_W, CELL_H, 12, 12); + + String label = c.state() == CellState.KNOWN ? "?" : c.name(); + int tw = g.getFontMetrics().stringWidth(label); + g.setColor(c.state() == CellState.KNOWN ? KNOWN.brighter() : stroke); + g.drawString(label, x + (CELL_W - tw) / 2, y + CELL_H / 2 + 4); + } + g.dispose(); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java index 1ef12f5..9d4a77e 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/SwingIO.java @@ -1,6 +1,7 @@ package thb.jeanluc.adventure.io; import thb.jeanluc.adventure.io.text.Hud; +import thb.jeanluc.adventure.io.text.MapView; import thb.jeanluc.adventure.io.text.RoomView; import thb.jeanluc.adventure.io.text.Span; import thb.jeanluc.adventure.io.text.Style; @@ -12,7 +13,6 @@ import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JScrollPane; -import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JTextPane; import javax.swing.KeyStroke; @@ -68,7 +68,7 @@ public class SwingIO implements GameIO { private final StyledDocument doc; private final JTextField input; private final JLabel hud; - private final JTextArea side; + private final MapPanel map; private final JScrollPane sideScroll; private final Font baseFont; @@ -95,12 +95,9 @@ public class SwingIO implements GameIO { hud.setForeground(new Color(0x8b, 0x94, 0xa3)); hud.setBorder(BorderFactory.createEmptyBorder(6, 12, 6, 12)); - side = new JTextArea(); - side.setEditable(false); - side.setBackground(new Color(0x0e, 0x12, 0x18)); - side.setForeground(new Color(0x6f, 0xcf, 0x73)); - side.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - sideScroll = new JScrollPane(side); + map = new MapPanel(baseFont); + sideScroll = new JScrollPane(map); + sideScroll.getViewport().setBackground(new Color(0x0e, 0x12, 0x18)); input = new JTextField(); input.addActionListener(e -> { @@ -185,7 +182,6 @@ public class SwingIO implements GameIO { output.setFont(baseFont.deriveFont(main)); input.setFont(baseFont.deriveFont(main)); hud.setFont(baseFont.deriveFont(small)); - side.setFont(baseFont.deriveFont(small)); frame.revalidate(); frame.repaint(); } @@ -285,9 +281,7 @@ public class SwingIO implements GameIO { spans.add(new Span(" is here.\n", Style.PLAIN)); } appendSpans(spans); - - String exits = room.exits().isEmpty() ? "(none)" : String.join("\n", room.exits()); - SwingUtilities.invokeLater(() -> side.setText("EXITS\n\n" + exits)); + // Exits are shown by the map panel, not as text. } @Override @@ -297,6 +291,16 @@ public class SwingIO implements GameIO { SwingUtilities.invokeLater(() -> hud.setText(txt)); } + @Override + public void setMap(MapView view) { + map.show(view); + } + + @Override + public void showMap(MapView view) { + // The GUI map panel is always visible; nothing extra to do on the 'map' command. + } + private void addList(List spans, List xs, Style st) { for (int i = 0; i < xs.size(); i++) { if (i > 0) {