Scrypt / space /static /play.html
IMJONEZZ's picture
play: reserve a bottom row + taller frame so the board prompt isn't clipped
a0de8fb
Raw
History Blame Contribute Delete
4.61 kB
<!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>