feat(io): SwingIO 4-region layout, JTextPane styling, bundled font loader
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,75 +1,143 @@
|
|||||||
package thb.jeanluc.adventure.io;
|
package thb.jeanluc.adventure.io;
|
||||||
|
|
||||||
|
import thb.jeanluc.adventure.io.text.Hud;
|
||||||
|
import thb.jeanluc.adventure.io.text.RoomView;
|
||||||
|
import thb.jeanluc.adventure.io.text.Span;
|
||||||
|
import thb.jeanluc.adventure.io.text.Style;
|
||||||
|
import thb.jeanluc.adventure.io.text.StyledText;
|
||||||
|
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JTextArea;
|
import javax.swing.JTextArea;
|
||||||
import javax.swing.JTextField;
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.JTextPane;
|
||||||
import javax.swing.SwingUtilities;
|
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.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
|
import java.awt.Insets;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import thb.jeanluc.adventure.io.text.StyledText;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link GameIO} backed by a Swing window. A read-only output area shows
|
* {@link GameIO} backed by a Swing window with four regions: a HUD bar (top),
|
||||||
* game text; a bottom text field collects player input. The input from
|
* a styled output pane (centre), a side panel for exits / future map (right),
|
||||||
* the EDT is handed to the (worker-thread) game loop through a
|
* and an input field (bottom). A bundled TrueType font is loaded if present so
|
||||||
* {@link LinkedBlockingQueue}.
|
* colours and glyphs work without the player installing anything.
|
||||||
*/
|
*/
|
||||||
public class SwingIO implements GameIO {
|
public class SwingIO implements GameIO {
|
||||||
|
|
||||||
/** Bridge between the EDT and the game loop. One element = one line. */
|
|
||||||
private final LinkedBlockingQueue<String> inputs = new LinkedBlockingQueue<>();
|
private final LinkedBlockingQueue<String> inputs = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
/** Main window. */
|
|
||||||
private final JFrame frame;
|
private final JFrame frame;
|
||||||
|
private final JTextPane output;
|
||||||
/** Output text area; appended to from any thread via {@link SwingUtilities#invokeLater}. */
|
private final StyledDocument doc;
|
||||||
private final JTextArea output;
|
|
||||||
|
|
||||||
/** Input text field; ENTER pushes the line into {@link #inputs}. */
|
|
||||||
private final JTextField input;
|
private final JTextField input;
|
||||||
|
private final JLabel hud;
|
||||||
|
private final JTextArea side;
|
||||||
|
private final Font baseFont;
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and shows the window with the given title. Must be called on
|
|
||||||
* the EDT (typical pattern: from {@code SwingUtilities.invokeLater}).
|
|
||||||
*
|
|
||||||
* @param title window title
|
|
||||||
*/
|
|
||||||
public SwingIO(String title) {
|
public SwingIO(String title) {
|
||||||
frame = new JFrame(title);
|
baseFont = loadFont();
|
||||||
output = new JTextArea();
|
|
||||||
input = new JTextField();
|
|
||||||
|
|
||||||
|
output = new JTextPane();
|
||||||
output.setEditable(false);
|
output.setEditable(false);
|
||||||
output.setLineWrap(true);
|
output.setBackground(new Color(0x0b, 0x0e, 0x13));
|
||||||
output.setWrapStyleWord(true);
|
output.setForeground(new Color(0xcf, 0xd6, 0xe0));
|
||||||
output.setBackground(Color.BLACK);
|
output.setFont(baseFont);
|
||||||
output.setForeground(Color.LIGHT_GRAY);
|
output.setMargin(new Insets(8, 8, 8, 8));
|
||||||
output.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
doc = output.getStyledDocument();
|
||||||
output.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
|
||||||
|
|
||||||
input.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
|
hud = new JLabel(" ");
|
||||||
|
hud.setOpaque(true);
|
||||||
|
hud.setBackground(new Color(0x11, 0x15, 0x1c));
|
||||||
|
hud.setForeground(new Color(0x8b, 0x94, 0xa3));
|
||||||
|
hud.setFont(baseFont.deriveFont(12f));
|
||||||
|
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.setFont(baseFont.deriveFont(12f));
|
||||||
|
side.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
JScrollPane sideScroll = new JScrollPane(side);
|
||||||
|
sideScroll.setPreferredSize(new Dimension(210, 0));
|
||||||
|
|
||||||
|
input = new JTextField();
|
||||||
|
input.setFont(baseFont);
|
||||||
input.addActionListener(e -> {
|
input.addActionListener(e -> {
|
||||||
String line = input.getText();
|
String line = input.getText();
|
||||||
input.setText("");
|
input.setText("");
|
||||||
output.append("> " + line + "\n");
|
appendSpans(List.of(new Span("> " + line + "\n", Style.DIM)));
|
||||||
inputs.offer(line);
|
inputs.offer(line);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frame = new JFrame(title);
|
||||||
frame.setLayout(new BorderLayout());
|
frame.setLayout(new BorderLayout());
|
||||||
|
frame.add(hud, BorderLayout.NORTH);
|
||||||
frame.add(new JScrollPane(output), BorderLayout.CENTER);
|
frame.add(new JScrollPane(output), BorderLayout.CENTER);
|
||||||
|
frame.add(sideScroll, BorderLayout.EAST);
|
||||||
frame.add(input, BorderLayout.SOUTH);
|
frame.add(input, BorderLayout.SOUTH);
|
||||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
frame.setSize(800, 600);
|
frame.setSize(900, 600);
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
frame.setVisible(true);
|
frame.setVisible(true);
|
||||||
input.requestFocusInWindow();
|
input.requestFocusInWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Font loadFont() {
|
||||||
|
try (InputStream in = getClass().getResourceAsStream("/fonts/game.ttf")) {
|
||||||
|
if (in != null) {
|
||||||
|
Font f = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(14f);
|
||||||
|
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(f);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// fall through to the logical monospaced fallback
|
||||||
|
}
|
||||||
|
return new Font(Font.MONOSPACED, Font.PLAIN, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendSpans(List<Span> spans) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
for (Span sp : 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);
|
||||||
|
}
|
||||||
|
output.setCaretPosition(doc.getLength());
|
||||||
|
} catch (BadLocationException ignored) {
|
||||||
|
// append-only; cannot occur with getLength()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String readLine() {
|
public String readLine() {
|
||||||
try {
|
try {
|
||||||
@@ -82,6 +150,44 @@ public class SwingIO implements GameIO {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void print(StyledText text) {
|
public void print(StyledText text) {
|
||||||
SwingUtilities.invokeLater(() -> output.append(text.plainText() + "\n"));
|
List<Span> spans = new ArrayList<>(text.spans());
|
||||||
|
spans.add(new Span("\n", Style.PLAIN));
|
||||||
|
appendSpans(spans);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showRoom(RoomView room) {
|
||||||
|
List<Span> spans = new ArrayList<>();
|
||||||
|
spans.add(new Span(room.name() + "\n", Style.HEADING));
|
||||||
|
spans.add(new Span(room.description().stripTrailing() + "\n", Style.PLAIN));
|
||||||
|
if (!room.items().isEmpty()) {
|
||||||
|
spans.add(new Span("You see ", Style.PLAIN));
|
||||||
|
addList(spans, room.items(), Style.ITEM);
|
||||||
|
spans.add(new Span(".\n", Style.PLAIN));
|
||||||
|
}
|
||||||
|
if (!room.npcs().isEmpty()) {
|
||||||
|
addList(spans, room.npcs(), Style.NPC);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHud(Hud h) {
|
||||||
|
String txt = " " + h.location() + " gold " + h.gold() + " turn " + h.turn()
|
||||||
|
+ " light " + (h.lightOn() ? "on" : "off");
|
||||||
|
SwingUtilities.invokeLater(() -> hud.setText(txt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addList(List<Span> spans, List<String> xs, Style st) {
|
||||||
|
for (int i = 0; i < xs.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
spans.add(new Span(", ", Style.PLAIN));
|
||||||
|
}
|
||||||
|
spans.add(new Span(xs.get(i), st));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
Semesterprojekt/src/main/resources/fonts/README.md
Normal file
12
Semesterprojekt/src/main/resources/fonts/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Bundled GUI font
|
||||||
|
|
||||||
|
Drop a TrueType font here named `game.ttf` to use it in the Swing GUI
|
||||||
|
(`SwingIO`). It is loaded via `Font.createFont` and registered at runtime —
|
||||||
|
no installation on the player's machine required.
|
||||||
|
|
||||||
|
- A monospaced font is recommended (the UI assumes fixed width for frames).
|
||||||
|
- A Nerd Font variant additionally provides icon glyphs.
|
||||||
|
- Check the font's licence before committing the `.ttf`.
|
||||||
|
|
||||||
|
If no `game.ttf` is present, `SwingIO` falls back to the logical
|
||||||
|
`Font.MONOSPACED`, so the game still runs.
|
||||||
Reference in New Issue
Block a user