feat(io): GUI quest-box under the map

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:33:57 +02:00
parent 310d476264
commit e3fb0c9fa6
2 changed files with 88 additions and 3 deletions

View File

@@ -0,0 +1,60 @@
package thb.jeanluc.adventure.io;
import thb.jeanluc.adventure.io.text.QuestText;
import thb.jeanluc.adventure.io.text.QuestView;
import thb.jeanluc.adventure.io.text.Span;
import thb.jeanluc.adventure.io.text.Style;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import java.awt.Color;
import java.awt.Font;
import java.awt.Insets;
/** Read-only styled view of the quest log, shown under the map. */
public class QuestPanel extends JTextPane {
public QuestPanel(Font font) {
setEditable(false);
setBackground(new Color(0x0b, 0x0e, 0x13));
setForeground(new Color(0xcf, 0xd6, 0xe0));
setFont(font.deriveFont(12f));
setMargin(new Insets(8, 10, 8, 10));
}
public void show(QuestView view) {
SwingUtilities.invokeLater(() -> {
StyledDocument doc = getStyledDocument();
try {
doc.remove(0, doc.getLength());
for (Span sp : QuestText.render(view).spans()) {
SimpleAttributeSet as = new SimpleAttributeSet();
StyleConstants.setForeground(as, colorFor(sp.style()));
if (sp.style() == Style.HEADING) {
StyleConstants.setBold(as, true);
}
doc.insertString(doc.getLength(), sp.text(), as);
}
setCaretPosition(0);
} catch (BadLocationException ignored) {
// replace-only; cannot occur with getLength()
}
});
}
private Color colorFor(Style s) {
return switch (s) {
case HEADING -> new Color(0xc9, 0x8a, 0xe0);
case ITEM -> new Color(0x46, 0xc8, 0xd8);
case NPC -> new Color(0xe6, 0xc3, 0x4a);
case EXIT -> new Color(0x6f, 0xcf, 0x73);
case DANGER -> new Color(0xe0, 0x6c, 0x6c);
case DIM -> new Color(0x6b, 0x72, 0x80);
case PLAIN -> new Color(0xcf, 0xd6, 0xe0);
};
}
}

View File

@@ -2,6 +2,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.QuestView;
import thb.jeanluc.adventure.io.text.RoomView;
import thb.jeanluc.adventure.io.text.Span;
import thb.jeanluc.adventure.io.text.Style;
@@ -12,6 +13,7 @@ import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
@@ -70,6 +72,9 @@ public class SwingIO implements GameIO {
private final JLabel hud;
private final MapPanel map;
private final JScrollPane sideScroll;
private final QuestPanel quests;
private final JScrollPane questScroll;
private final JPanel sidePanel;
private final Font baseFont;
/** Detected display scale (≥ 1.0); HiDPI panels report > 1.0 where supported. */
@@ -99,6 +104,14 @@ public class SwingIO implements GameIO {
sideScroll = new JScrollPane(map);
sideScroll.getViewport().setBackground(new Color(0x0e, 0x12, 0x18));
quests = new QuestPanel(baseFont);
questScroll = new JScrollPane(quests);
questScroll.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(0x2a, 0x2f, 0x3a)));
sidePanel = new JPanel(new BorderLayout());
sidePanel.add(sideScroll, BorderLayout.CENTER);
sidePanel.add(questScroll, BorderLayout.SOUTH);
input = new JTextField();
input.addActionListener(e -> {
String line = input.getText();
@@ -111,7 +124,7 @@ public class SwingIO implements GameIO {
frame.setLayout(new BorderLayout());
frame.add(hud, BorderLayout.NORTH);
frame.add(new JScrollPane(output), BorderLayout.CENTER);
frame.add(sideScroll, BorderLayout.EAST);
frame.add(sidePanel, BorderLayout.EAST);
frame.add(input, BorderLayout.SOUTH);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize((int) (900 * uiScale), (int) (600 * uiScale));
@@ -171,8 +184,9 @@ public class SwingIO implements GameIO {
int min = (int) (SIDE_MIN * uiScale);
int max = (int) (SIDE_MAX * uiScale);
int target = Math.max(min, Math.min(max, (int) (frame.getWidth() * SIDE_RATIO)));
sideScroll.setPreferredSize(new Dimension(target, 0));
sideScroll.revalidate();
sidePanel.setPreferredSize(new Dimension(target, 0));
questScroll.setPreferredSize(new Dimension(target, (int) (frame.getHeight() * 0.32)));
sidePanel.revalidate();
frame.validate();
// The side panel changed size: let the map re-fit into it.
map.revalidate();
@@ -197,6 +211,7 @@ public class SwingIO implements GameIO {
output.setFont(baseFont.deriveFont(main));
input.setFont(baseFont.deriveFont(main));
hud.setFont(baseFont.deriveFont(small));
quests.setFont(baseFont.deriveFont(small));
map.setZoom(zoom);
frame.revalidate();
frame.repaint();
@@ -312,6 +327,16 @@ public class SwingIO implements GameIO {
map.show(view);
}
@Override
public void setQuests(QuestView view) {
quests.show(view);
}
@Override
public void showQuests(QuestView view) {
// The GUI quest box is always visible; nothing extra to do.
}
@Override
public void showMap(MapView view) {
// The GUI map panel is always visible; nothing extra to do on the 'map' command.