/** * lanternfall in a terminal — the same dungeon, the same database, the same * shared map. `ssh` in and you are another delver on the floor; the web * players can see you standing there, and you can see them. * * hjkl or the arrows move, `.` waits and rests, `>` and `<` take stairs, * `q` leaves. Your name comes from the account at the keyboard, so the * gravestone will have it on. */ import * as store from "./db.ts"; import { HEIGHT, WIDTH, floorAt } from "./dungeon.ts"; import { act, breedOf, cacheCells, delverFor, isAction, viewFor } from "./game.ts"; import { ordinal } from "./names.ts"; import { WELCOME_LINES } from "./pages.ts"; const db = store.open(); const hand = `tty:${process.env.FORGE_APP_USER || "guest"}`; const esc = "\x1b["; const out = (s: string): void => { process.stdout.write(s); }; const paint = (code: number, text: string, bold = false): string => `${esc}${bold ? "1;" : ""}38;5;${code}m${text}${esc}0m`; const DARK = 234; const MEMORY = 239; const LIT_FLOOR = 137; const LIT_WALL = 95; const STAIR = 71; const GOLD = 178; const BONE = 183; const BEAST = 167; const OTHER = 74; const YOU = 230; function draw(): void { const delver = delverFor(db, hand); const view = viewFor(db, delver); const caches = store.cacheState(db, delver.depth, store.turn(db)); out(`${esc}2J${esc}H`); out( `${paint(GOLD, "lanternfall", true)} ${paint(MEMORY, `· ${ordinal(delver.depth)} floor · turn ${delver.turns}`)}\r\n\r\n`, ); for (let y = 0; y < HEIGHT; y += 1) { let line = " "; for (let x = 0; x < WIDTH; x += 1) { const cell = y * WIDTH + x; const tile = view.floor.tiles[cell] ?? "#"; const lit = view.visible.has(cell); const known = view.seen.has(cell); if (cell === delver.cell) { line += paint(YOU, "@", true); continue; } if (!lit && !known) { line += " "; continue; } if (lit) { const monster = view.monsters.find((m) => m.cell === cell); if (monster) { line += paint(BEAST, breedOf(monster.kind).glyph, true); continue; } if (view.others.has(cell)) { line += paint(OTHER, "@", true); continue; } if (caches.has(cell) === false && view.floor.tiles[cell] === "." && isCache(view, cell)) { line += paint(GOLD, "$"); continue; } } if (view.graves.has(cell)) { line += paint(lit ? BONE : MEMORY, "†"); continue; } if (tile === ">" || tile === "<") { line += paint(lit ? STAIR : MEMORY, tile, lit); continue; } if (!lit) { line += paint(MEMORY, tile === "#" ? "#" : "·"); continue; } line += paint(tile === "#" ? LIT_WALL : LIT_FLOOR, tile === "#" ? "#" : "·"); } out(`${line}\r\n`); } const bar = `${"♥".repeat(Math.max(0, delver.hp))}${paint(DARK, "·".repeat(Math.max(0, delver.max_hp - delver.hp)))}`; out(`\r\n ${paint(BEAST, bar)} ${paint(GOLD, `${delver.gold}g`)} ${paint(MEMORY, delver.name)}\r\n`); // What happened this turn in ink; what the log remembers, dim. Before // anything has happened, the same welcome the page gives. const heard = store.heard(db, delver.id, 4); if (heard.length === 0) for (const line of WELCOME_LINES) out(` ${paint(YOU, line)}\r\n`); for (const line of heard) out(` ${paint(line.turn === delver.turns ? YOU : MEMORY, line.text)}\r\n`); out(`\r\n ${paint(DARK, "hjkl/arrows move · . rest · > descend · < climb · q quit")}\r\n`); } // A cache is only a rumour until you are standing next to it; the glyph shows // where one is *known* to sit, which is what the web page does too. function isCache(view: ReturnType, cell: number): boolean { return cacheCellsFor(view.floor.depth).includes(cell); } const cacheMemo = new Map(); function cacheCellsFor(depth: number): number[] { const had = cacheMemo.get(depth); if (had) return had; const cells = cacheCells(floorAt(depth)); cacheMemo.set(depth, cells); return cells; } const KEYS: Record = { h: "west", j: "south", k: "north", l: "east", [`${esc}A`]: "north", [`${esc}B`]: "south", [`${esc}C`]: "east", [`${esc}D`]: "west", ".": "wait", " ": "wait", ">": "descend", "<": "ascend", }; function key(data: Buffer): void { const pressed = data.toString("utf8"); if (pressed === "q" || pressed === "\x03" || pressed === "\x04") { out(`${esc}2J${esc}H`); db.close(); process.exit(0); } const action = KEYS[pressed]; if (action !== undefined && isAction(action)) act(db, hand, action); draw(); } if (process.stdin.isTTY) process.stdin.setRawMode(true); process.stdin.on("data", key); process.stdout.on("resize", draw); // Somebody else's turn is your turn too: the floor moves while you stand still. setInterval(draw, 3000); draw();