File size: 4,607 Bytes
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34b513d
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
6f42620
 
 
 
 
 
9fca766
 
 
6f42620
9fca766
 
 
 
 
34b513d
 
 
 
9fca766
 
 
 
34b513d
 
 
 
 
 
 
 
6f42620
 
 
 
 
 
 
 
 
 
 
 
 
 
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
a0de8fb
 
 
 
 
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f42620
 
9fca766
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SCRYPT — session</title>
<link rel="stylesheet" href="/static/app.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" />
</head>
<body>
<main class="room">
  <section class="term-frame">
    <div class="term-bar">
      <span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
      <span class="title">warden@scryptos — tty1</span>
      <a href="/">◄ leave</a>
    </div>
    <div id="terminal"></div>
    <div class="term-status" id="status">connecting to the machine…</div>
  </section>
</main>

<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.18.0/lib/addon-webgl.min.js"></script>
<script>
// Osaka Jade, mapped onto an xterm 16-colour palette so the game's Rich
// styles land on exactly the colours the terminal build uses.
const JADE = {
  background: "#111c18", foreground: "#c1c497", cursor: "#d7c995",
  cursorAccent: "#111c18", selectionBackground: "#2c4a3a",
  black: "#23372b", red: "#ff5345", green: "#549e6a", yellow: "#459451",
  blue: "#509475", magenta: "#d2689c", cyan: "#2dd5b7", white: "#f6f5dd",
  brightBlack: "#53685b", brightRed: "#db9f9c", brightGreen: "#9eebb3",
  brightYellow: "#e5c736", brightBlue: "#acd4cf", brightMagenta: "#75bbb3",
  brightCyan: "#8cd3cb", brightWhite: "#f6f5dd",
};

// The board needs this much terminal real estate; below it, cards clip.
const MIN_ROWS = 44;
const MIN_COLS = 110;
const MAX_FONT = 15;
const MIN_FONT = 10;

const term = new Terminal({
  theme: JADE,
  fontFamily: "'IBM Plex Mono', ui-monospace, monospace",
  fontSize: MAX_FONT,
  lineHeight: 1.05,
  cursorBlink: true,
  cursorStyle: "block",
  allowProposedApi: true,
  scrollback: 2000,
  // Card frames, scales, and shading are box-drawing/block glyphs; let
  // xterm draw those itself (cell-perfect) instead of trusting the webfont,
  // whose substituted glyphs warp the card art.
  customGlyphs: true,
});
const fit = new FitAddon.FitAddon();
term.loadAddon(fit);
term.open(document.getElementById("terminal"));
// customGlyphs needs the WebGL renderer; without it xterm falls back to the
// DOM renderer, which hands the art back to the font. Fall back gracefully
// on machines without WebGL.
try {
  const webgl = new WebglAddon.WebglAddon();
  webgl.onContextLoss(() => webgl.dispose());
  term.loadAddon(webgl);
} catch (e) { /* DOM renderer: playable, just softer card frames */ }

// Fit the whole board, not just the window: start at the comfy font size and
// step down until the grid clears MIN_ROWS x MIN_COLS (or we hit the floor).
function fitGame() {
  let size = MAX_FONT;
  term.options.fontSize = size;
  fit.fit();
  while ((term.rows < MIN_ROWS || term.cols < MIN_COLS) && size > MIN_FONT) {
    size -= 1;
    term.options.fontSize = size;
    fit.fit();
  }
}
fitGame();

const status = document.getElementById("status");
function setStatus(html, dead = false) {
  status.innerHTML = html;
  status.classList.toggle("dead", dead);
}

const proto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${location.host}/pty`);
ws.binaryType = "arraybuffer";

function sendResize() {
  if (ws.readyState === WebSocket.OPEN) {
    // Reserve one row: the board docks its instruction prompt to the bottom
    // row, and FitAddon tends to report one more row than is actually
    // painted — without this margin that prompt renders just off-screen.
    const rows = Math.max(1, term.rows - 1);
    ws.send(JSON.stringify({ resize: [term.cols, rows] }));
  }
}

ws.onopen = () => {
  setStatus("the Warden is <b>watching</b> · type to play");
  sendResize();
  term.focus();
};
ws.onmessage = (ev) => {
  const bytes = typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
  term.write(bytes);
};
ws.onclose = () => setStatus("the machine closed the session. <b>refresh to wake it again.</b>", true);
ws.onerror = () => setStatus("connection severed.", true);

term.onData((data) => {
  if (ws.readyState === WebSocket.OPEN) ws.send(new TextEncoder().encode(data));
});

window.addEventListener("resize", () => { fitGame(); sendResize(); });
const ro = new ResizeObserver(() => { fitGame(); sendResize(); });
ro.observe(document.getElementById("terminal"));
</script>
</body>
</html>