UNPKG

shellquest

Version:

A terminal-based dungeon crawler game with ASCII graphics

1,597 lines (1,578 loc) 405 kB
// @bun var __require = import.meta.require; // src/core/ansi.ts var ANSI = { switchToAlternateScreen: "\x1B[?1049h", switchToMainScreen: "\x1B[?1049l", reset: "\x1B[0m", hideCursor: "\x1B[?25l", showCursor: "\x1B[?25h", resetCursorColor: "\x1B]12;default\x07", saveCursorState: "\x1B[s", restoreCursorState: "\x1B[u", enableMouseTracking: "\x1B[?1000h", disableMouseTracking: "\x1B[?1000l", enableButtonEventTracking: "\x1B[?1002h", disableButtonEventTracking: "\x1B[?1002l", enableAnyEventTracking: "\x1B[?1003h", disableAnyEventTracking: "\x1B[?1003l", enableSGRMouseMode: "\x1B[?1006h", disableSGRMouseMode: "\x1B[?1006l" }; // src/core/Renderable.ts import { EventEmitter } from "events"; var renderableNumber = 1; class Renderable extends EventEmitter { static renderablesByNumber = new Map; id; num; ctx = null; _x; _y; _width; _height; _zIndex; visible; selectable = false; renderableMap = new Map; renderableArray = []; needsZIndexSort = false; parent = null; constructor(id, options) { super(); this.id = id; this.num = renderableNumber++; this._x = options.x; this._y = options.y; this._width = options.width; this._height = options.height; this._zIndex = options.zIndex; this.visible = options.visible !== false; Renderable.renderablesByNumber.set(this.num, this); } hasSelection() { return false; } onSelectionChanged(selection) { return false; } getSelectedText() { return ""; } shouldStartSelection(x, y) { return false; } set needsUpdate(value) { if (this.parent) { this.parent.needsUpdate = value; } } get x() { if (this.parent) { return this.parent.x + this._x; } return this._x; } set x(value) { this._x = value; } get y() { if (this.parent) { return this.parent.y + this._y; } return this._y; } set y(value) { this._y = value; } get width() { return this._width; } set width(value) { this._width = value; } get height() { return this._height; } set height(value) { this._height = value; } get zIndex() { return this._zIndex; } set zIndex(value) { if (this._zIndex !== value) { this._zIndex = value; this.parent?.requestZIndexSort(); } } requestZIndexSort() { this.needsZIndexSort = true; } ensureZIndexSorted() { if (this.needsZIndexSort) { this.renderableArray.sort((a, b) => a.zIndex > b.zIndex ? 1 : a.zIndex < b.zIndex ? -1 : 0); this.needsZIndexSort = false; } } clear() { for (const child of this.renderableArray) { this.remove(child.id); } } add(obj) { if (this.renderableMap.has(obj.id)) { this.remove(obj.id); } if (obj.parent) { obj.parent.remove(obj.id); } obj.parent = this; if (this.ctx) { obj.ctx = this.ctx; } this.renderableArray.push(obj); this.needsZIndexSort = true; this.renderableMap.set(obj.id, obj); this.emit("child:added", obj); } propagateContext(ctx) { this.ctx = ctx; for (const child of this.renderableArray) { child.propagateContext(ctx); } } unload() {} getRenderable(id) { return this.renderableMap.get(id); } remove(id) { if (!id) { return; } if (this.renderableMap.has(id)) { const obj = this.renderableMap.get(id); if (obj) { obj.parent = null; obj.propagateContext(null); obj.unload(); } this.renderableMap.delete(id); const index = this.renderableArray.findIndex((obj2) => obj2.id === id); if (index !== -1) { this.renderableArray.splice(index, 1); } this.emit("child:removed", id); } } getAllElementIds() { return Array.from(this.renderableMap.keys()); } getChildren() { return [...this.renderableArray]; } render(buffer, deltaTime) { if (!this.visible) return; this.renderSelf(buffer, deltaTime); this.ctx?.addToHitGrid(this.x, this.y, this.width, this.height, this.num); this.ensureZIndexSorted(); for (const child of this.renderableArray) { child.render(buffer, deltaTime); } } renderSelf(buffer, deltaTime) {} destroy() { if (this.parent) { throw new Error(`Cannot destroy ${this.id} while it still has a parent (${this.parent.id}). Remove from parent first.`); } for (const child of this.renderableArray) { child.parent = null; child.destroy(); } this.renderableArray = []; this.renderableMap.clear(); Renderable.renderablesByNumber.delete(this.num); this.destroySelf(); } destroySelf() {} processMouseEvent(event) { this.onMouseEvent(event); if (this.parent && !event.defaultPrevented) { this.parent.processMouseEvent(event); } } onMouseEvent(event) {} } // src/core/types.ts class RGBA { buffer; constructor(buffer) { this.buffer = buffer; } static fromArray(array) { return new RGBA(array); } static fromValues(r, g, b, a = 1) { return new RGBA(new Float32Array([r, g, b, a])); } static fromInts(r, g, b, a = 255) { return new RGBA(new Float32Array([r / 255, g / 255, b / 255, a / 255])); } static fromHex(hex) { if (typeof hex === "number") { return hexToRgb(`#${hex.toString(16).padStart(6, "0")}`); } else if (!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)) { return hexToRgb(hex); } } get r() { return this.buffer[0]; } set r(value) { this.buffer[0] = value; } get g() { return this.buffer[1]; } set g(value) { this.buffer[1] = value; } get b() { return this.buffer[2]; } set b(value) { this.buffer[2] = value; } get a() { return this.buffer[3]; } set a(value) { this.buffer[3] = value; } map(fn) { return [fn(this.r), fn(this.g), fn(this.b), fn(this.a)]; } toString() { return `rgba(${this.r.toFixed(2)}, ${this.g.toFixed(2)}, ${this.b.toFixed(2)}, ${this.a.toFixed(2)})`; } } var TextAttributes = { NONE: 0, BOLD: 1 << 0, DIM: 1 << 1, ITALIC: 1 << 2, UNDERLINE: 1 << 3, BLINK: 1 << 4, INVERSE: 1 << 5, HIDDEN: 1 << 6, STRIKETHROUGH: 1 << 7 }; var DebugOverlayCorner; ((DebugOverlayCorner2) => { DebugOverlayCorner2[DebugOverlayCorner2["topLeft"] = 0] = "topLeft"; DebugOverlayCorner2[DebugOverlayCorner2["topRight"] = 1] = "topRight"; DebugOverlayCorner2[DebugOverlayCorner2["bottomLeft"] = 2] = "bottomLeft"; DebugOverlayCorner2[DebugOverlayCorner2["bottomRight"] = 3] = "bottomRight"; })(DebugOverlayCorner ||= {}); // src/core/utils.ts function hexToRgb(hex) { hex = hex.replace(/^#/, ""); if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } if (!/^[0-9A-Fa-f]{6}$/.test(hex)) { console.warn(`Invalid hex color: ${hex}, defaulting to magenta`); return RGBA.fromValues(1, 0, 1, 1); } const r = parseInt(hex.substring(0, 2), 16) / 255; const g = parseInt(hex.substring(2, 4), 16) / 255; const b = parseInt(hex.substring(4, 6), 16) / 255; return RGBA.fromValues(r, g, b, 1); } function rgbToHex(rgb) { return "#" + [rgb.r, rgb.g, rgb.b].map((x) => { const hex = Math.floor(Math.max(0, Math.min(1, x) * 255)).toString(16); return hex.length === 1 ? "0" + hex : hex; }).join(""); } function hsvToRgb(h, s, v) { let r = 0, g = 0, b = 0; const i = Math.floor(h / 60) % 6; const f = h / 60 - Math.floor(h / 60); const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s); switch (i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; } return RGBA.fromValues(r, g, b, 1); } var CSS_COLOR_NAMES = { black: "#000000", white: "#FFFFFF", red: "#FF0000", green: "#008000", blue: "#0000FF", yellow: "#FFFF00", cyan: "#00FFFF", magenta: "#FF00FF", silver: "#C0C0C0", gray: "#808080", grey: "#808080", maroon: "#800000", olive: "#808000", lime: "#00FF00", aqua: "#00FFFF", teal: "#008080", navy: "#000080", fuchsia: "#FF00FF", purple: "#800080", orange: "#FFA500", brightblack: "#666666", brightred: "#FF6666", brightgreen: "#66FF66", brightblue: "#6666FF", brightyellow: "#FFFF66", brightcyan: "#66FFFF", brightmagenta: "#FF66FF", brightwhite: "#FFFFFF" }; function parseColor(color) { if (typeof color === "string") { const lowerColor = color.toLowerCase(); if (lowerColor === "transparent") { return RGBA.fromValues(0, 0, 0, 0); } if (CSS_COLOR_NAMES[lowerColor]) { return hexToRgb(CSS_COLOR_NAMES[lowerColor]); } return hexToRgb(color); } return color; } function createTextAttributes({ bold = false, italic = false, underline = false, dim = false, blink = false, inverse = false, hidden = false, strikethrough = false } = {}) { let attributes = TextAttributes.NONE; if (bold) attributes |= TextAttributes.BOLD; if (italic) attributes |= TextAttributes.ITALIC; if (underline) attributes |= TextAttributes.UNDERLINE; if (dim) attributes |= TextAttributes.DIM; if (blink) attributes |= TextAttributes.BLINK; if (inverse) attributes |= TextAttributes.INVERSE; if (hidden) attributes |= TextAttributes.HIDDEN; if (strikethrough) attributes |= TextAttributes.STRIKETHROUGH; return attributes; } async function loadTemplate(filePath, params) { const template = await Bun.file(filePath).text(); return template.replace(/\${(\w+)}/g, (match, key) => params[key] || match); } function fixPaths(paths) { if (process.env.BUN_PACKER_BUNDLE) { return Object.fromEntries(Object.entries(paths).map(([key, value]) => [key, value.replace("../", "")])); } return paths; } // src/core/ui/lib/border.ts var BorderChars = { single: { topLeft: "\u250C", topRight: "\u2510", bottomLeft: "\u2514", bottomRight: "\u2518", horizontal: "\u2500", vertical: "\u2502", topT: "\u252C", bottomT: "\u2534", leftT: "\u251C", rightT: "\u2524", cross: "\u253C" }, double: { topLeft: "\u2554", topRight: "\u2557", bottomLeft: "\u255A", bottomRight: "\u255D", horizontal: "\u2550", vertical: "\u2551", topT: "\u2566", bottomT: "\u2569", leftT: "\u2560", rightT: "\u2563", cross: "\u256C" }, rounded: { topLeft: "\u256D", topRight: "\u256E", bottomLeft: "\u2570", bottomRight: "\u256F", horizontal: "\u2500", vertical: "\u2502", topT: "\u252C", bottomT: "\u2534", leftT: "\u251C", rightT: "\u2524", cross: "\u253C" }, heavy: { topLeft: "\u250F", topRight: "\u2513", bottomLeft: "\u2517", bottomRight: "\u251B", horizontal: "\u2501", vertical: "\u2503", topT: "\u2533", bottomT: "\u253B", leftT: "\u2523", rightT: "\u252B", cross: "\u254B" } }; function getBorderFromSides(sides) { const result = []; if (sides.top) result.push("top"); if (sides.right) result.push("right"); if (sides.bottom) result.push("bottom"); if (sides.left) result.push("left"); return result.length > 0 ? result : false; } function getBorderSides(border) { return border === true ? { top: true, right: true, bottom: true, left: true } : Array.isArray(border) ? { top: border.includes("top"), right: border.includes("right"), bottom: border.includes("bottom"), left: border.includes("left") } : { top: false, right: false, bottom: false, left: false }; } function drawBorder(buffer, options) { const borderColor = parseColor(options.borderColor); const backgroundColor = parseColor(options.backgroundColor); const borderSides = getBorderSides(options.border); const borders = options.customBorderChars || BorderChars[options.borderStyle]; const startX = Math.max(0, options.x); const startY = Math.max(0, options.y); const endX = Math.min(buffer.getWidth() - 1, options.x + options.width - 1); const endY = Math.min(buffer.getHeight() - 1, options.y + options.height - 1); const drawTop = borderSides.top; let shouldDrawTitle = false; let titleX = startX; let titleStartX = 0; let titleEndX = 0; if (options.title && options.title.length > 0 && drawTop) { const titleLength = options.title.length; const minTitleSpace = 4; shouldDrawTitle = options.width >= titleLength + minTitleSpace; if (shouldDrawTitle) { const padding = 2; if (options.titleAlignment === "center") { titleX = startX + Math.max(padding, Math.floor((options.width - titleLength) / 2)); } else if (options.titleAlignment === "right") { titleX = startX + options.width - padding - titleLength; } else { titleX = startX + padding; } titleX = Math.max(startX + padding, Math.min(titleX, endX - titleLength)); titleStartX = titleX; titleEndX = titleX + titleLength - 1; } } const drawBottom = borderSides.bottom; const drawLeft = borderSides.left; const drawRight = borderSides.right; const leftBorderOnly = drawLeft && !drawTop && !drawBottom; const rightBorderOnly = drawRight && !drawTop && !drawBottom; const bottomOnlyWithVerticals = drawBottom && !drawTop && (drawLeft || drawRight); const topOnlyWithVerticals = drawTop && !drawBottom && (drawLeft || drawRight); const extendVerticalsToTop = leftBorderOnly || rightBorderOnly || bottomOnlyWithVerticals; const extendVerticalsToBottom = leftBorderOnly || rightBorderOnly || topOnlyWithVerticals; if (drawTop || drawBottom) { if (drawTop) { for (let x = startX;x <= endX; x++) { if (startY >= 0 && startY < buffer.getHeight()) { let char = borders.horizontal; if (x === startX) { char = drawLeft ? borders.topLeft : borders.horizontal; } else if (x === endX) { char = drawRight ? borders.topRight : borders.horizontal; } if (shouldDrawTitle && x >= titleStartX && x <= titleEndX) { continue; } buffer.setCellWithAlphaBlending(x, startY, char, borderColor, backgroundColor); } } } if (drawBottom) { for (let x = startX;x <= endX; x++) { if (endY >= 0 && endY < buffer.getHeight()) { let char = borders.horizontal; if (x === startX) { char = drawLeft ? borders.bottomLeft : borders.horizontal; } else if (x === endX) { char = drawRight ? borders.bottomRight : borders.horizontal; } buffer.setCellWithAlphaBlending(x, endY, char, borderColor, backgroundColor); } } } } const verticalStartY = extendVerticalsToTop ? startY : startY + (drawTop ? 1 : 0); const verticalEndY = extendVerticalsToBottom ? endY : endY - (drawBottom ? 1 : 0); if (drawLeft || drawRight) { for (let y = verticalStartY;y <= verticalEndY; y++) { if (drawLeft && startX >= 0 && startX < buffer.getWidth()) { buffer.setCellWithAlphaBlending(startX, y, borders.vertical, borderColor, backgroundColor); } if (drawRight && endX >= 0 && endX < buffer.getWidth()) { buffer.setCellWithAlphaBlending(endX, y, borders.vertical, borderColor, backgroundColor); } } } if (shouldDrawTitle && options.title) { buffer.drawText(options.title, titleX, startY, borderColor, backgroundColor, 0); } } // src/core/selection.ts class Selection { _anchor; _focus; _selectedRenderables = []; constructor(anchor, focus) { this._anchor = { ...anchor }; this._focus = { ...focus }; } get anchor() { return { ...this._anchor }; } get focus() { return { ...this._focus }; } get bounds() { return { startX: Math.min(this._anchor.x, this._focus.x), startY: Math.min(this._anchor.y, this._focus.y), endX: Math.max(this._anchor.x, this._focus.x), endY: Math.max(this._anchor.y, this._focus.y) }; } updateSelectedRenderables(selectedRenderables) { this._selectedRenderables = selectedRenderables; } getSelectedText() { const selectedTexts = this._selectedRenderables.sort((a, b) => { const aY = a.y; const bY = b.y; if (aY !== bY) { return aY - bY; } return a.x - b.x; }).map((renderable) => renderable.getSelectedText()).filter((text) => text); return selectedTexts.join(` `); } } class TextSelectionHelper { getX; getY; getTextLength; getLineInfo; localSelection = null; cachedGlobalSelection = null; constructor(getX, getY, getTextLength, getLineInfo) { this.getX = getX; this.getY = getY; this.getTextLength = getTextLength; this.getLineInfo = getLineInfo; } hasSelection() { return this.localSelection !== null; } getSelection() { return this.localSelection; } reevaluateSelection(width, height = 1) { if (!this.cachedGlobalSelection) { return false; } return this.onSelectionChanged(this.cachedGlobalSelection, width, height); } shouldStartSelection(x, y, width, height) { const localX = x - this.getX(); const localY = y - this.getY(); return localX >= 0 && localX < width && localY >= 0 && localY < height; } onSelectionChanged(selection, width, height = 1) { this.cachedGlobalSelection = selection; const previousSelection = this.localSelection; if (!selection?.isActive) { this.localSelection = null; return previousSelection !== null; } const myY = this.getY(); const myEndY = myY + height - 1; if (myEndY < selection.anchor.y || myY > selection.focus.y) { this.localSelection = null; return previousSelection !== null; } if (height === 1) { this.localSelection = this.calculateSingleLineSelection(myY, selection.anchor.y, selection.focus.y, selection.anchor.x, selection.focus.x, width); } else { this.localSelection = this.calculateMultiLineSelection(myY, selection.anchor.y, selection.focus.y, selection.anchor.x, selection.focus.x); } return this.localSelection !== null !== (previousSelection !== null) || this.localSelection?.start !== previousSelection?.start || this.localSelection?.end !== previousSelection?.end; } calculateSingleLineSelection(lineY, anchorY, focusY, anchorX, focusX, width) { const textLength = this.getTextLength(); const myX = this.getX(); if (lineY > anchorY && lineY < focusY) { return { start: 0, end: textLength }; } if (lineY === anchorY && lineY === focusY) { const start = Math.max(0, Math.min(anchorX - myX, textLength)); const end = Math.max(0, Math.min(focusX - myX, textLength)); return start < end ? { start, end } : null; } if (lineY === anchorY) { const start = Math.max(0, Math.min(anchorX - myX, textLength)); return start < textLength ? { start, end: textLength } : null; } if (lineY === focusY) { const end = Math.max(0, Math.min(focusX - myX, textLength)); return end > 0 ? { start: 0, end } : null; } return null; } calculateMultiLineSelection(startY, anchorY, focusY, anchorX, focusX) { const lineInfo = this.getLineInfo?.(); if (!lineInfo) { return { start: 0, end: this.getTextLength() }; } const myX = this.getX(); let selectionStart = null; let selectionEnd = null; for (let i = 0;i < lineInfo.lineStarts.length; i++) { const lineY = startY + i; if (lineY < anchorY || lineY > focusY) continue; const lineStart = lineInfo.lineStarts[i]; const lineEnd = i < lineInfo.lineStarts.length - 1 ? lineInfo.lineStarts[i + 1] - 1 : this.getTextLength(); const lineWidth = lineInfo.lineWidths[i]; if (lineY > anchorY && lineY < focusY) { if (selectionStart === null) selectionStart = lineStart; selectionEnd = lineEnd; } else if (lineY === anchorY && lineY === focusY) { const localStartX = Math.max(0, Math.min(anchorX - myX, lineWidth)); const localEndX = Math.max(0, Math.min(focusX - myX, lineWidth)); if (localStartX < localEndX) { selectionStart = lineStart + localStartX; selectionEnd = lineStart + localEndX; } } else if (lineY === anchorY) { const localStartX = Math.max(0, Math.min(anchorX - myX, lineWidth)); if (localStartX < lineWidth) { selectionStart = lineStart + localStartX; selectionEnd = lineEnd; } } else if (lineY === focusY) { const localEndX = Math.max(0, Math.min(focusX - myX, lineWidth)); if (localEndX > 0) { if (selectionStart === null) selectionStart = lineStart; selectionEnd = lineStart + localEndX; } } } return selectionStart !== null && selectionEnd !== null && selectionStart < selectionEnd ? { start: selectionStart, end: selectionEnd } : null; } } // src/core/objects.ts function sanitizeText(text, tabStopWidth) { return text.replace(/\t/g, " ".repeat(tabStopWidth)); } class TextRenderable extends Renderable { selectable = true; _content = ""; _fg; _bg; attributes = 0; tabStopWidth = 2; selectionHelper; constructor(id, options) { super(id, { ...options, width: 0, height: 0 }); const fgRgb = parseColor(options.fg || RGBA.fromInts(255, 255, 255, 255)); this.selectionHelper = new TextSelectionHelper(() => this.x, () => this.y, () => this._content.length); this.tabStopWidth = options.tabStopWidth || 2; this.setContent(options.content); this._fg = fgRgb; this._bg = options.bg !== undefined ? parseColor(options.bg) : RGBA.fromValues(0, 0, 0, 0); this.attributes = options.attributes || 0; } setContent(value) { this._content = sanitizeText(value, this.tabStopWidth); this.width = this._content.length; this.height = 1; const changed = this.selectionHelper.reevaluateSelection(this.width); if (changed) { this.needsUpdate = true; } } get fg() { return this._fg; } get bg() { return this._bg; } set fg(value) { if (value) { this._fg = parseColor(value); this.needsUpdate = true; } } set bg(value) { if (value) { this._bg = parseColor(value); this.needsUpdate = true; } } set content(value) { this.setContent(value); this.needsUpdate = true; } get content() { return this._content; } shouldStartSelection(x, y) { return this.selectionHelper.shouldStartSelection(x, y, this.width, this.height); } onSelectionChanged(selection) { const changed = this.selectionHelper.onSelectionChanged(selection, this.width); if (changed) { this.needsUpdate = true; } return this.selectionHelper.hasSelection(); } getSelectedText() { const selection = this.selectionHelper.getSelection(); if (!selection) return ""; return this._content.slice(selection.start, selection.end); } hasSelection() { return this.selectionHelper.hasSelection(); } renderSelf(buffer) { const selection = this.selectionHelper.getSelection(); buffer.drawText(this._content, this.x, this.y, this._fg, this._bg, this.attributes, selection); } } class BoxRenderable extends Renderable { _bg; _border; _borderStyle; borderColor; customBorderChars; borderSides; shouldFill; title; titleAlignment; constructor(id, options) { super(id, options); const bgRgb = parseColor(options.bg); const borderRgb = parseColor(options.borderColor || RGBA.fromValues(255, 255, 255, 255)); this.width = options.width; this.height = options.height; this._bg = bgRgb; this._border = options.border ?? true; this._borderStyle = options.borderStyle || "single"; this.borderColor = borderRgb; this.customBorderChars = options.customBorderChars || BorderChars[this._borderStyle]; this.borderSides = getBorderSides(this._border); this.shouldFill = options.shouldFill !== false; this.title = options.title; this.titleAlignment = options.titleAlignment || "left"; } get bg() { return this._bg; } set bg(value) { if (value) { this._bg = parseColor(value); } } set border(value) { this._border = value; this.borderSides = getBorderSides(value); this.needsUpdate = true; } set borderStyle(value) { this._borderStyle = value; this.customBorderChars = BorderChars[this._borderStyle]; this.needsUpdate = true; } renderSelf(buffer) { if (this.x >= buffer.getWidth() || this.y >= buffer.getHeight() || this.x + this.width <= 0 || this.y + this.height <= 0) { return; } const startX = Math.max(0, this.x); const startY = Math.max(0, this.y); const endX = Math.min(buffer.getWidth() - 1, this.x + this.width - 1); const endY = Math.min(buffer.getHeight() - 1, this.y + this.height - 1); if (this.shouldFill) { if (this.border === false) { buffer.fillRect(startX, startY, endX - startX + 1, endY - startY + 1, this._bg); } else { const innerStartX = startX + (this.borderSides.left ? 1 : 0); const innerStartY = startY + (this.borderSides.top ? 1 : 0); const innerEndX = endX - (this.borderSides.right ? 1 : 0); const innerEndY = endY - (this.borderSides.bottom ? 1 : 0); if (innerEndX >= innerStartX && innerEndY >= innerStartY) { buffer.fillRect(innerStartX, innerStartY, innerEndX - innerStartX + 1, innerEndY - innerStartY + 1, this._bg); } } } if (this.border !== false) { drawBorder(buffer, { x: this.x, y: this.y, width: this.width, height: this.height, borderStyle: this._borderStyle, border: this._border, borderColor: this.borderColor, backgroundColor: this._bg, customBorderChars: this.customBorderChars, title: this.title, titleAlignment: this.titleAlignment }); } } } class FrameBufferRenderable extends Renderable { frameBuffer; constructor(id, buffer, options) { super(id, options); this.frameBuffer = buffer; } renderSelf(buffer) { buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer); } destroySelf() { this.frameBuffer.destroy(); } } class GroupRenderable extends Renderable { constructor(id, options) { super(id, { ...options, width: 0, height: 0 }); } has(id) { return this.renderableMap.has(id); } } class StyledTextRenderable extends Renderable { selectable = true; frameBuffer; _fragment; _defaultFg; _defaultBg; _selectionBg; _selectionFg; selectionHelper; _plainText = ""; _lineInfo = { lineStarts: [], lineWidths: [] }; constructor(id, buffer, options) { super(id, options); this.selectionHelper = new TextSelectionHelper(() => this.x, () => this.y, () => this._plainText.length, () => this._lineInfo); this.frameBuffer = buffer; this._fragment = options.fragment; this._defaultFg = options.defaultFg ? parseColor(options.defaultFg) : RGBA.fromValues(1, 1, 1, 1); this._defaultBg = options.defaultBg ? parseColor(options.defaultBg) : RGBA.fromValues(0, 0, 0, 0); this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined; this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined; this.updateTextInfo(); this.renderFragmentToBuffer(); } get fragment() { return this._fragment; } set fragment(value) { this._fragment = value; this.updateTextInfo(); this.renderFragmentToBuffer(); this.needsUpdate = true; } get defaultFg() { return this._defaultFg; } set defaultFg(value) { if (value) { this._defaultFg = parseColor(value); this.renderFragmentToBuffer(); this.needsUpdate = true; } } get defaultBg() { return this._defaultBg; } set defaultBg(value) { if (value) { this._defaultBg = parseColor(value); this.renderFragmentToBuffer(); this.needsUpdate = true; } } updateTextInfo() { this._plainText = this._fragment.toString(); this._lineInfo.lineStarts = [0]; this._lineInfo.lineWidths = []; let currentLineWidth = 0; for (let i = 0;i < this._plainText.length; i++) { if (this._plainText[i] === ` `) { this._lineInfo.lineWidths.push(currentLineWidth); this._lineInfo.lineStarts.push(i + 1); currentLineWidth = 0; } else { currentLineWidth++; } } this._lineInfo.lineWidths.push(currentLineWidth); const changed = this.selectionHelper.reevaluateSelection(this.width, this.height); if (changed) { this.renderFragmentToBuffer(); this.needsUpdate = true; } } shouldStartSelection(x, y) { return this.selectionHelper.shouldStartSelection(x, y, this.width, this.height); } onSelectionChanged(selection) { const changed = this.selectionHelper.onSelectionChanged(selection, this.width, this.height); if (changed) { this.renderFragmentToBuffer(); this.needsUpdate = true; } return this.selectionHelper.hasSelection(); } getSelectedText() { const selection = this.selectionHelper.getSelection(); if (!selection) return ""; return this._plainText.slice(selection.start, selection.end); } hasSelection() { return this.selectionHelper.hasSelection(); } renderFragmentToBuffer() { this.frameBuffer.clear(this._defaultBg); const selection = this.selectionHelper.getSelection(); this.frameBuffer.drawStyledTextFragment(this._fragment, 0, 0, this._defaultFg, this._defaultBg, selection ? { ...selection, bgColor: this._selectionBg, fgColor: this._selectionFg } : undefined); } renderSelf(buffer) { buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer); } destroySelf() { this.frameBuffer.destroy(); } } // src/core/zig.ts import { dlopen, suffix, toArrayBuffer } from "bun:ffi"; import { join } from "path"; import { existsSync } from "fs"; import os from "os"; // src/core/buffer.ts var fbIdCounter = 0; function isRGBAWithAlpha(color) { return color.a < 1; } function blendColors(overlay, text) { const [overlayR, overlayG, overlayB, overlayA] = overlay.buffer; const [textR, textG, textB, textA] = text.buffer; if (overlayA === 1) { return overlay; } const alpha = overlayA; let perceptualAlpha; if (alpha > 0.8) { const normalizedHighAlpha = (alpha - 0.8) * 5; const curvedHighAlpha = Math.pow(normalizedHighAlpha, 0.2); perceptualAlpha = 0.8 + curvedHighAlpha * 0.2; } else { perceptualAlpha = Math.pow(alpha, 0.9); } const r = overlayR * perceptualAlpha + textR * (1 - perceptualAlpha); const g = overlayG * perceptualAlpha + textG * (1 - perceptualAlpha); const b = overlayB * perceptualAlpha + textB * (1 - perceptualAlpha); return RGBA.fromValues(r, g, b, textA); } class OptimizedBuffer { id; lib; bufferPtr; buffer; width; height; respectAlpha = false; useFFI = true; get ptr() { return this.bufferPtr; } constructor(lib, ptr, buffer, width, height, options) { this.id = `fb_${fbIdCounter++}`; this.lib = lib; this.respectAlpha = options.respectAlpha || false; this.width = width; this.height = height; this.bufferPtr = ptr; this.buffer = buffer; } static create(width, height, options = {}) { const lib = resolveRenderLib(); const respectAlpha = options.respectAlpha || false; return lib.createOptimizedBuffer(width, height, respectAlpha); } get buffers() { return this.buffer; } coordsToIndex(x, y) { return y * this.width + x; } getWidth() { return this.width; } getHeight() { return this.height; } setRespectAlpha(respectAlpha) { this.lib.bufferSetRespectAlpha(this.bufferPtr, respectAlpha); this.respectAlpha = respectAlpha; } clear(bg = RGBA.fromValues(0, 0, 0, 1)) { if (this.useFFI) { this.clearFFI(bg); } else { this.clearLocal(bg); } } clearLocal(bg = RGBA.fromValues(0, 0, 0, 1)) { this.buffer.char.fill(32); this.buffer.attributes.fill(0); for (let i = 0;i < this.width * this.height; i++) { const index = i * 4; this.buffer.fg[index] = 1; this.buffer.fg[index + 1] = 1; this.buffer.fg[index + 2] = 1; this.buffer.fg[index + 3] = 1; this.buffer.bg[index] = bg.r; this.buffer.bg[index + 1] = bg.g; this.buffer.bg[index + 2] = bg.b; this.buffer.bg[index + 3] = bg.a; } } setCell(x, y, char, fg, bg, attributes = 0) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) return; const index = this.coordsToIndex(x, y); const colorIndex = index * 4; this.buffer.char[index] = typeof char === "string" ? char.charCodeAt(0) : char; this.buffer.attributes[index] = attributes; this.buffer.fg[colorIndex] = fg.r; this.buffer.fg[colorIndex + 1] = fg.g; this.buffer.fg[colorIndex + 2] = fg.b; this.buffer.fg[colorIndex + 3] = fg.a; this.buffer.bg[colorIndex] = bg.r; this.buffer.bg[colorIndex + 1] = bg.g; this.buffer.bg[colorIndex + 2] = bg.b; this.buffer.bg[colorIndex + 3] = bg.a; } getCell(x, y) { return this.get(x, y); } get(x, y) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) return null; const index = this.coordsToIndex(x, y); const colorIndex = index * 4; return { char: this.buffer.char[index], fg: RGBA.fromArray(this.buffer.fg.slice(colorIndex, colorIndex + 4)), bg: RGBA.fromArray(this.buffer.bg.slice(colorIndex, colorIndex + 4)), attributes: this.buffer.attributes[index] }; } setCellWithAlphaBlending(x, y, char, fg, bg, attributes = 0) { if (this.useFFI) { this.setCellWithAlphaBlendingFFI(x, y, char, fg, bg, attributes); } else { this.setCellWithAlphaBlendingLocal(x, y, char, fg, bg, attributes); } } setCellWithAlphaBlendingLocal(x, y, char, fg, bg, attributes = 0) { if (x < 0 || x >= this.width || y < 0 || y >= this.height) return; const hasBgAlpha = isRGBAWithAlpha(bg); const hasFgAlpha = isRGBAWithAlpha(fg); if (hasBgAlpha || hasFgAlpha) { const destCell = this.get(x, y); if (destCell) { const blendedBgRgb = hasBgAlpha ? blendColors(bg, destCell.bg) : bg; const preserveChar = char === " " && destCell.char !== 0 && String.fromCharCode(destCell.char) !== " "; const finalChar = preserveChar ? destCell.char : char.charCodeAt(0); let finalFg; if (preserveChar) { finalFg = blendColors(bg, destCell.fg); } else { finalFg = hasFgAlpha ? blendColors(fg, destCell.bg) : fg; } const finalAttributes = preserveChar ? destCell.attributes : attributes; const finalBg = RGBA.fromValues(blendedBgRgb.r, blendedBgRgb.g, blendedBgRgb.b, bg.a); this.setCell(x, y, String.fromCharCode(finalChar), finalFg, finalBg, finalAttributes); return; } } this.setCell(x, y, char, fg, bg, attributes); } drawText(text, x, y, fg, bg, attributes = 0, selection) { const method = this.useFFI ? this.drawTextFFI : this.drawTextLocal; if (!selection) { method.call(this, text, x, y, fg, bg, attributes); return; } const { start, end } = selection; let selectionBg; let selectionFg; if (selection.bgColor) { selectionBg = selection.bgColor; selectionFg = selection.fgColor || fg; } else { const defaultBg = bg || RGBA.fromValues(0, 0, 0, 0); selectionFg = defaultBg.a > 0 ? defaultBg : RGBA.fromValues(0, 0, 0, 1); selectionBg = fg; } if (start > 0) { const beforeText = text.slice(0, start); method.call(this, beforeText, x, y, fg, bg, attributes); } if (end > start) { const selectedText = text.slice(start, end); method.call(this, selectedText, x + start, y, selectionFg, selectionBg, attributes); } if (end < text.length) { const afterText = text.slice(end); method.call(this, afterText, x + end, y, fg, bg, attributes); } } drawTextLocal(text, x, y, fg, bg, attributes = 0) { if (y < 0 || y >= this.height) return; if (!text || typeof text !== "string") { console.warn("drawTextLocal called with invalid text:", { text, x, y, fg, bg }); return; } let startX = this.width; let endX = 0; let i = 0; for (const char of text) { const charX = x + i; i++; if (charX < 0 || charX >= this.width) continue; startX = Math.min(startX, charX); endX = Math.max(endX, charX); let bgColor = bg; if (!bgColor) { const existingCell = this.get(charX, y); if (existingCell) { bgColor = existingCell.bg; } else { bgColor = RGBA.fromValues(0, 0, 0, 1); } } this.setCellWithAlphaBlending(charX, y, char, fg, bgColor, attributes); } } fillRect(x, y, width, height, bg) { if (this.useFFI) { this.fillRectFFI(x, y, width, height, bg); } else { this.fillRectLocal(x, y, width, height, bg); } } fillRectLocal(x, y, width, height, bg) { const startX = Math.max(0, x); const startY = Math.max(0, y); const endX = Math.min(this.getWidth() - 1, x + width - 1); const endY = Math.min(this.getHeight() - 1, y + height - 1); if (startX > endX || startY > endY) return; const hasAlpha = isRGBAWithAlpha(bg); if (hasAlpha) { const fg = RGBA.fromValues(1, 1, 1, 1); for (let fillY = startY;fillY <= endY; fillY++) { for (let fillX = startX;fillX <= endX; fillX++) { this.setCellWithAlphaBlending(fillX, fillY, " ", fg, bg, 0); } } } else { for (let fillY = startY;fillY <= endY; fillY++) { for (let fillX = startX;fillX <= endX; fillX++) { const index = this.coordsToIndex(fillX, fillY); const colorIndex = index * 4; this.buffer.char[index] = 32; this.buffer.attributes[index] = 0; this.buffer.fg[colorIndex] = 1; this.buffer.fg[colorIndex + 1] = 1; this.buffer.fg[colorIndex + 2] = 1; this.buffer.fg[colorIndex + 3] = 1; this.buffer.bg[colorIndex] = bg.r; this.buffer.bg[colorIndex + 1] = bg.g; this.buffer.bg[colorIndex + 2] = bg.b; this.buffer.bg[colorIndex + 3] = bg.a; } } } } drawFrameBuffer(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight) { this.drawFrameBufferFFI(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight); } drawFrameBufferLocal(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight) { const srcX = sourceX ?? 0; const srcY = sourceY ?? 0; const srcWidth = sourceWidth ?? frameBuffer.getWidth(); const srcHeight = sourceHeight ?? frameBuffer.getHeight(); if (srcX >= frameBuffer.getWidth() || srcY >= frameBuffer.getHeight()) return; if (srcWidth === 0 || srcHeight === 0) return; const clampedSrcWidth = Math.min(srcWidth, frameBuffer.getWidth() - srcX); const clampedSrcHeight = Math.min(srcHeight, frameBuffer.getHeight() - srcY); const startDestX = Math.max(0, destX); const startDestY = Math.max(0, destY); const endDestX = Math.min(this.width - 1, destX + clampedSrcWidth - 1); const endDestY = Math.min(this.height - 1, destY + clampedSrcHeight - 1); if (!frameBuffer.respectAlpha) { for (let dY = startDestY;dY <= endDestY; dY++) { for (let dX = startDestX;dX <= endDestX; dX++) { const relativeDestX = dX - destX; const relativeDestY = dY - destY; const sX = srcX + relativeDestX; const sY = srcY + relativeDestY; if (sX >= frameBuffer.getWidth() || sY >= frameBuffer.getHeight()) continue; const destIndex = this.coordsToIndex(dX, dY); const srcIndex = frameBuffer.coordsToIndex(sX, sY); const destColorIndex = destIndex * 4; const srcColorIndex = srcIndex * 4; this.buffer.char[destIndex] = frameBuffer.buffer.char[srcIndex]; this.buffer.attributes[destIndex] = frameBuffer.buffer.attributes[srcIndex]; this.buffer.fg[destColorIndex] = frameBuffer.buffer.fg[srcColorIndex]; this.buffer.fg[destColorIndex + 1] = frameBuffer.buffer.fg[srcColorIndex + 1]; this.buffer.fg[destColorIndex + 2] = frameBuffer.buffer.fg[srcColorIndex + 2]; this.buffer.fg[destColorIndex + 3] = frameBuffer.buffer.fg[srcColorIndex + 3]; this.buffer.bg[destColorIndex] = frameBuffer.buffer.bg[srcColorIndex]; this.buffer.bg[destColorIndex + 1] = frameBuffer.buffer.bg[srcColorIndex + 1]; this.buffer.bg[destColorIndex + 2] = frameBuffer.buffer.bg[srcColorIndex + 2]; this.buffer.bg[destColorIndex + 3] = frameBuffer.buffer.bg[srcColorIndex + 3]; } } return; } for (let dY = startDestY;dY <= endDestY; dY++) { for (let dX = startDestX;dX <= endDestX; dX++) { const relativeDestX = dX - destX; const relativeDestY = dY - destY; const sX = srcX + relativeDestX; const sY = srcY + relativeDestY; if (sX >= frameBuffer.getWidth() || sY >= frameBuffer.getHeight()) continue; const srcIndex = frameBuffer.coordsToIndex(sX, sY); const srcColorIndex = srcIndex * 4; if (frameBuffer.buffer.bg[srcColorIndex + 3] === 0 && frameBuffer.buffer.fg[srcColorIndex + 3] === 0) { continue; } const charCode = frameBuffer.buffer.char[srcIndex]; const fg = RGBA.fromArray(frameBuffer.buffer.fg.slice(srcColorIndex, srcColorIndex + 4)); const bg = RGBA.fromArray(frameBuffer.buffer.bg.slice(srcColorIndex, srcColorIndex + 4)); const attributes = frameBuffer.buffer.attributes[srcIndex]; this.setCellWithAlphaBlending(dX, dY, String.fromCharCode(charCode), fg, bg, attributes); } } } destroy() { this.lib.destroyOptimizedBuffer(this.bufferPtr); } drawStyledText(styledText, x, y, defaultFg = RGBA.fromValues(1, 1, 1, 1), defaultBg = RGBA.fromValues(0, 0, 0, 0)) { this.drawStyledTextLocal(styledText, x, y, defaultFg, defaultBg); } drawStyledTextLocal(styledText, x, y, defaultFg = RGBA.fromValues(1, 1, 1, 1), defaultBg = RGBA.fromValues(0, 0, 0, 0), selection) { let currentX = x; let currentY = y; let charIndex = 0; for (const styledChar of styledText) { if (styledChar.char === ` `) { currentY++; currentX = x; charIndex++; continue; } let fg = styledChar.style.fg ? parseColor(styledChar.style.fg) : defaultFg; let bg = styledChar.style.bg ? parseColor(styledChar.style.bg) : defaultBg; const isSelected = selection && charIndex >= selection.start && charIndex < selection.end; if (isSelected) { if (selection.bgColor) { bg = selection.bgColor; if (selection.fgColor) { fg = selection.fgColor; } } else { const temp = fg; fg = bg.a > 0 ? bg : RGBA.fromValues(0, 0, 0, 1); bg = temp; } } if (styledChar.style.reverse) { [fg, bg] = [bg, fg]; } const attributes = createTextAttributes({ bold: styledChar.style.bold, italic: styledChar.style.italic, underline: styledChar.style.underline, dim: styledChar.style.dim, blink: styledChar.style.blink, inverse: styledChar.style.reverse, hidden: false, strikethrough: styledChar.style.strikethrough }); this.setCellWithAlphaBlending(currentX, currentY, styledChar.char, fg, bg, attributes); currentX++; charIndex++; } } drawStyledTextFragment(fragment, x, y, defaultFg, defaultBg, selection) { this.drawStyledTextFragmentLocal(fragment, x, y, defaultFg, defaultBg, selection); } drawStyledTextFragmentLocal(fragment, x, y, defaultFg, defaultBg, selection) { this.drawStyledTextLocal(fragment.toStyledText(), x, y, defaultFg, defaultBg, selection); } drawSuperSampleBuffer(x, y, pixelDataPtr, pixelDataLength, format, alignedBytesPerRow) { this.drawSuperSampleBufferFFI(x, y, pixelDataPtr, pixelDataLength, format, alignedBytesPerRow); } drawSuperSampleBufferFFI(x, y, pixelDataPtr, pixelDataLength, format, alignedBytesPerRow) { this.lib.bufferDrawSuperSampleBuffer(this.bufferPtr, x, y, pixelDataPtr, pixelDataLength, format, alignedBytesPerRow); } drawPackedBuffer(dataPtr, dataLen, posX, posY, terminalWidthCells, terminalHeightCells) { this.lib.bufferDrawPackedBuffer(this.bufferPtr, dataPtr, dataLen, posX, posY, terminalWidthCells, terminalHeightCells); } setCellWithAlphaBlendingFFI(x, y, char, fg, bg, attributes) { this.lib.bufferSetCellWithAlphaBlending(this.bufferPtr, x, y, char, fg, bg, attributes); } fillRectFFI(x, y, width, height, bg) { this.lib.bufferFillRect(this.bufferPtr, x, y, width, height, bg); } resize(width, height) { if (this.width === width && this.height === height) return; this.width = width; this.height = height; this.buffer = this.lib.bufferResize(this.bufferPtr, width, height); } clearFFI(bg = RGBA.fromValues(0, 0, 0, 1)) { this.lib.bufferClear(this.bufferPtr, bg); } drawTextFFI(text, x, y, fg = RGBA.fromValues(1, 1, 1, 1), bg, attributes = 0) { this.lib.bufferDrawText(this.bufferPtr, text, x, y, fg, bg, attributes); } drawFrameBufferFFI(destX, destY, frameBuffer, sourceX, sourceY, sourceWidth, sourceHeight) { this.lib.drawFrameBuffer(this.bufferPtr, destX, destY, frameBuffer.ptr, sourceX, sourceY, sourceWidth, sourceHeight); } } // src/core/zig.ts var __dirname = "G:\\code\\shellquest\\game\\src\\core"; function getPlatformTarget() { const platform = os.platform(); const arch = os.arch(); const platformMap = { darwin: "macos", win32: "windows", linux: "linux" }; const archMap = { x64: "x86_64", arm64: "aarch64" }; const zigPlatform = platformMap[platform] || platform; const zigArch = archMap[arch] || arch; return `${zigArch}-${zigPlatform}`; } function findLibrary() { const target = getPlatformTarget(); const [arch, os2] = target.split("-"); const isWindows = os2 === "windows"; const libraryName = isWindows ? "opentui" : "libopentui"; let entryDir; try { entryDir = typeof Bun !== "undefined" && Bun.main ? __require("path").dirname(Bun.main) : undefined; } catch {} if (!entryDir && process.argv[1]) { entryDir = __require("path").dirname(process.argv[1]); } if (entryDir) { const candidate = join(entryDir, "dist", "zig", "lib", target, `${libraryName}.${suffix}`); if (existsSync(candidate)) { return candidate; } let dir = entryDir; for (let i = 0;i < 4; ++i) { dir = __require("path").dirname(dir); const candidate2 = join(dir, "dist", "zig", "lib", target, `${libraryName}.${suffix}`); if (existsSync(candidate2)) { return candidate2; } } } const possiblePaths = [ join(process.cwd(), "dist", "zig", "lib", target), join(process.cwd(), "node_modules", "shellquest.sh", "dist", "zig", "lib", target), join(process.cwd(), "node_modules", "shellquest", "dist", "zig", "lib", target), join(process.cwd(), "node_modules", "tui-crawler", "dist", "zig", "lib", target), join(process.cwd(), "src", "zig", "lib", target), join(__dirname, "zig", "lib", target), join(__dirname, "..", "zig", "lib", target), join(__dirname, "..", "..", "zig", "lib", target) ]; for (const basePath of possiblePaths) { const targetLibPath = join(basePath, `${libraryName}.${suffix}`); if (existsSync(targetLibPath)) { return targetLibPath; } } throw new Error(`Could not find opentui library for platform: ${target}`); } function getOpenTUILib(libPath) { const resolvedLibPath = libPath || findLibrary(); return dlopen(resolvedLibPath, { createRenderer: { args: ["u32", "u32"], returns: "ptr" }, destroyRenderer: { args: ["ptr"], returns: "void" }, setUseThread: { args: ["ptr", "bool"], returns: "void" }, setBackgroundColor: { args: ["ptr", "ptr"], returns: "void" }, updateStats: { args: ["ptr", "f64", "u32", "f64"], returns: "void" }, updateMemoryStats: { args: ["ptr", "u32", "u32", "u32"], returns: "void" }, render: { args: ["ptr"], returns: "void" }, getNextBuffer: { args: ["ptr"], returns: "ptr" }, getCurrentBuffer: { args: ["ptr"], returns: "ptr" }, createOptimizedBuffer: { args: ["u32", "u32", "bool"], returns: "ptr" }, destroyOptimizedBuffer: { args: ["ptr"], returns: "void" }, drawFrameBuffer: { args: ["ptr", "i32", "i32", "ptr", "u32", "u32", "u32", "u32"], returns: "void" }, getBufferWidth: { args: ["ptr"], returns: "u32" }, getBufferHeight: { args: ["ptr"], returns: "u32" }, bufferClear: { args: ["ptr", "ptr"], returns: "void" }, bufferGetCharPtr: { args: ["ptr"], returns: "ptr" }, bufferGetFgPtr: { args: ["ptr"], returns: "ptr" }, bufferGetBgPtr: { args: ["ptr"], returns: "ptr"