adding the table helper
This commit is contained in:
parent
1e024b4b2c
commit
25815aa4d2
3 changed files with 359 additions and 23 deletions
|
|
@ -149,6 +149,11 @@ class SceneManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (block.type === "table") {
|
||||||
|
await this._renderTable(block);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Text with optional className (supports html: true for HTML content)
|
// Text with optional className (supports html: true for HTML content)
|
||||||
if (block.text !== undefined) {
|
if (block.text !== undefined) {
|
||||||
if (block.html) {
|
if (block.html) {
|
||||||
|
|
@ -379,6 +384,64 @@ class SceneManager {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render a dynamic table with conditional row support
|
||||||
|
async _renderTable(block) {
|
||||||
|
// Filter rows based on conditions
|
||||||
|
const filteredRows = [];
|
||||||
|
for (const row of block.rows || []) {
|
||||||
|
// Row can be: array of cells, or object with { cells, condition, className }
|
||||||
|
if (Array.isArray(row)) {
|
||||||
|
// Simple row - array of cells
|
||||||
|
filteredRows.push(row);
|
||||||
|
} else if (row.condition !== undefined) {
|
||||||
|
// Conditional row
|
||||||
|
if (this.state.evaluate(row.condition)) {
|
||||||
|
filteredRows.push(this._processTableRow(row));
|
||||||
|
}
|
||||||
|
} else if (row.cells) {
|
||||||
|
// Object row without condition
|
||||||
|
filteredRows.push(this._processTableRow(row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build table using TableHelper
|
||||||
|
const tableOutput = TableHelper.table({
|
||||||
|
title: block.title ? this._interpolateText(block.title) : undefined,
|
||||||
|
headers: block.headers,
|
||||||
|
rows: filteredRows,
|
||||||
|
widths: block.widths,
|
||||||
|
align: block.align,
|
||||||
|
style: block.style || "single",
|
||||||
|
padding: block.padding,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render each line of the table
|
||||||
|
for (const line of tableOutput) {
|
||||||
|
if (typeof line === "string") {
|
||||||
|
this._printText(line);
|
||||||
|
} else if (line.text) {
|
||||||
|
this._printText(line.text, line.className || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a table row object into cell array format for TableHelper
|
||||||
|
_processTableRow(row) {
|
||||||
|
// If row has className, apply it to cells that don't have their own
|
||||||
|
const cells = row.cells.map((cell) => {
|
||||||
|
if (typeof cell === "string" && row.className) {
|
||||||
|
return { text: this._interpolateText(cell), className: row.className };
|
||||||
|
} else if (typeof cell === "object" && cell.text) {
|
||||||
|
return {
|
||||||
|
text: this._interpolateText(cell.text),
|
||||||
|
className: cell.className || row.className,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return typeof cell === "string" ? this._interpolateText(cell) : cell;
|
||||||
|
});
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
// Get current scene ID
|
// Get current scene ID
|
||||||
getCurrentSceneId() {
|
getCurrentSceneId() {
|
||||||
return this.currentScene ? this.currentScene.id : null;
|
return this.currentScene ? this.currentScene.id : null;
|
||||||
|
|
|
||||||
241
assets/js/games/engine/table-helper.js
Normal file
241
assets/js/games/engine/table-helper.js
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
// Table Helper - Generates monospace box-drawn tables for terminal games
|
||||||
|
|
||||||
|
const TableHelper = {
|
||||||
|
// Box drawing characters
|
||||||
|
chars: {
|
||||||
|
single: {
|
||||||
|
topLeft: "┌",
|
||||||
|
topRight: "┐",
|
||||||
|
bottomLeft: "└",
|
||||||
|
bottomRight: "┘",
|
||||||
|
horizontal: "─",
|
||||||
|
vertical: "│",
|
||||||
|
leftT: "├",
|
||||||
|
rightT: "┤",
|
||||||
|
topT: "┬",
|
||||||
|
bottomT: "┴",
|
||||||
|
cross: "┼",
|
||||||
|
},
|
||||||
|
double: {
|
||||||
|
topLeft: "╔",
|
||||||
|
topRight: "╗",
|
||||||
|
bottomLeft: "╚",
|
||||||
|
bottomRight: "╝",
|
||||||
|
horizontal: "═",
|
||||||
|
vertical: "║",
|
||||||
|
leftT: "╠",
|
||||||
|
rightT: "╣",
|
||||||
|
topT: "╦",
|
||||||
|
bottomT: "╩",
|
||||||
|
cross: "╬",
|
||||||
|
},
|
||||||
|
ascii: {
|
||||||
|
topLeft: "+",
|
||||||
|
topRight: "+",
|
||||||
|
bottomLeft: "+",
|
||||||
|
bottomRight: "+",
|
||||||
|
horizontal: "-",
|
||||||
|
vertical: "|",
|
||||||
|
leftT: "+",
|
||||||
|
rightT: "+",
|
||||||
|
topT: "+",
|
||||||
|
bottomT: "+",
|
||||||
|
cross: "+",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a complete table with headers and rows
|
||||||
|
* @param {Object} options - Table configuration
|
||||||
|
* @param {string} [options.title] - Optional title for the table
|
||||||
|
* @param {string[]} [options.headers] - Column headers
|
||||||
|
* @param {Array<Array<string|{text: string, className?: string}>>} options.rows - Table data rows
|
||||||
|
* @param {number[]} [options.widths] - Column widths (auto-calculated if not provided)
|
||||||
|
* @param {string[]} [options.align] - Column alignments ('left', 'right', 'center')
|
||||||
|
* @param {string} [options.style='single'] - Border style ('single', 'double', 'ascii')
|
||||||
|
* @param {number} [options.padding=1] - Cell padding
|
||||||
|
* @returns {Array<string|{text: string, className?: string}>} Array of content blocks
|
||||||
|
*/
|
||||||
|
table(options) {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
headers,
|
||||||
|
rows = [],
|
||||||
|
widths: customWidths,
|
||||||
|
align = [],
|
||||||
|
style = "single",
|
||||||
|
padding = 1,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const c = this.chars[style] || this.chars.single;
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
// Calculate column widths
|
||||||
|
const widths = customWidths || this._calculateWidths(headers, rows, padding);
|
||||||
|
const totalWidth = widths.reduce((a, b) => a + b, 0) + widths.length + 1;
|
||||||
|
|
||||||
|
// Top border
|
||||||
|
output.push(c.topLeft + c.horizontal.repeat(totalWidth - 2) + c.topRight);
|
||||||
|
|
||||||
|
// Title (if provided)
|
||||||
|
if (title) {
|
||||||
|
output.push(this._centerText(title, totalWidth, c.vertical));
|
||||||
|
output.push(c.leftT + c.horizontal.repeat(totalWidth - 2) + c.rightT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers (if provided)
|
||||||
|
if (headers) {
|
||||||
|
output.push(this._formatRow(headers, widths, align, c.vertical, padding));
|
||||||
|
output.push(c.leftT + c.horizontal.repeat(totalWidth - 2) + c.rightT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data rows
|
||||||
|
for (const row of rows) {
|
||||||
|
const formattedRow = this._formatRow(row, widths, align, c.vertical, padding);
|
||||||
|
|
||||||
|
// Check if any cell has a className
|
||||||
|
const hasClassName = row.some(cell => cell && typeof cell === "object" && cell.className);
|
||||||
|
|
||||||
|
if (hasClassName) {
|
||||||
|
// Find the className from the row (use first one found)
|
||||||
|
const className = row.find(cell => cell && typeof cell === "object" && cell.className)?.className;
|
||||||
|
output.push({ text: formattedRow, className });
|
||||||
|
} else {
|
||||||
|
output.push(formattedRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom border
|
||||||
|
output.push(c.bottomLeft + c.horizontal.repeat(totalWidth - 2) + c.bottomRight);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a simple bordered box with text
|
||||||
|
* @param {string|string[]} content - Content to display
|
||||||
|
* @param {Object} [options] - Box options
|
||||||
|
* @param {number} [options.width] - Box width (auto if not set)
|
||||||
|
* @param {string} [options.style='single'] - Border style
|
||||||
|
* @param {string} [options.align='left'] - Text alignment
|
||||||
|
* @returns {string[]} Array of strings
|
||||||
|
*/
|
||||||
|
box(content, options = {}) {
|
||||||
|
const { width: customWidth, style = "single", align = "left" } = options;
|
||||||
|
const c = this.chars[style] || this.chars.single;
|
||||||
|
const lines = Array.isArray(content) ? content : [content];
|
||||||
|
|
||||||
|
const maxLineWidth = Math.max(...lines.map(l => this._textLength(l)));
|
||||||
|
const width = customWidth || maxLineWidth + 4;
|
||||||
|
const innerWidth = width - 2;
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
output.push(c.topLeft + c.horizontal.repeat(innerWidth) + c.topRight);
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const text = typeof line === "string" ? line : line.text || "";
|
||||||
|
const padded = this._alignText(text, innerWidth - 2, align);
|
||||||
|
output.push(c.vertical + " " + padded + " " + c.vertical);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(c.bottomLeft + c.horizontal.repeat(innerWidth) + c.bottomRight);
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a separator line
|
||||||
|
* @param {number} width - Line width
|
||||||
|
* @param {string} [style='single'] - Border style
|
||||||
|
* @param {string} [type='middle'] - 'top', 'middle', 'bottom'
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
separator(width, style = "single", type = "middle") {
|
||||||
|
const c = this.chars[style] || this.chars.single;
|
||||||
|
const inner = c.horizontal.repeat(width - 2);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "top":
|
||||||
|
return c.topLeft + inner + c.topRight;
|
||||||
|
case "bottom":
|
||||||
|
return c.bottomLeft + inner + c.bottomRight;
|
||||||
|
default:
|
||||||
|
return c.leftT + inner + c.rightT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pad or truncate text to exact width
|
||||||
|
* @param {string} text - Input text
|
||||||
|
* @param {number} width - Target width
|
||||||
|
* @param {string} [align='left'] - Alignment
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
pad(text, width, align = "left") {
|
||||||
|
return this._alignText(text, width, align);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Internal helpers
|
||||||
|
|
||||||
|
_calculateWidths(headers, rows, padding) {
|
||||||
|
const allRows = headers ? [headers, ...rows] : rows;
|
||||||
|
const numCols = Math.max(...allRows.map(r => r.length));
|
||||||
|
const widths = new Array(numCols).fill(0);
|
||||||
|
|
||||||
|
for (const row of allRows) {
|
||||||
|
for (let i = 0; i < row.length; i++) {
|
||||||
|
const cell = row[i];
|
||||||
|
const text = typeof cell === "object" ? (cell.text || "") : String(cell || "");
|
||||||
|
widths[i] = Math.max(widths[i], text.length + padding * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return widths;
|
||||||
|
},
|
||||||
|
|
||||||
|
_formatRow(row, widths, alignments, verticalChar, padding) {
|
||||||
|
const cells = [];
|
||||||
|
for (let i = 0; i < widths.length; i++) {
|
||||||
|
const cell = row[i];
|
||||||
|
const text = typeof cell === "object" ? (cell.text || "") : String(cell || "");
|
||||||
|
const align = alignments[i] || "left";
|
||||||
|
const cellWidth = widths[i] - padding * 2;
|
||||||
|
const padChar = " ".repeat(padding);
|
||||||
|
cells.push(padChar + this._alignText(text, cellWidth, align) + padChar);
|
||||||
|
}
|
||||||
|
return verticalChar + cells.join(verticalChar) + verticalChar;
|
||||||
|
},
|
||||||
|
|
||||||
|
_centerText(text, totalWidth, verticalChar) {
|
||||||
|
const innerWidth = totalWidth - 2;
|
||||||
|
const padded = this._alignText(text, innerWidth, "center");
|
||||||
|
return verticalChar + padded + verticalChar;
|
||||||
|
},
|
||||||
|
|
||||||
|
_alignText(text, width, align) {
|
||||||
|
const len = text.length;
|
||||||
|
if (len >= width) {
|
||||||
|
return text.substring(0, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = width - len;
|
||||||
|
switch (align) {
|
||||||
|
case "right":
|
||||||
|
return " ".repeat(diff) + text;
|
||||||
|
case "center":
|
||||||
|
const left = Math.floor(diff / 2);
|
||||||
|
const right = diff - left;
|
||||||
|
return " ".repeat(left) + text + " ".repeat(right);
|
||||||
|
default:
|
||||||
|
return text + " ".repeat(diff);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_textLength(item) {
|
||||||
|
if (typeof item === "string") return item.length;
|
||||||
|
if (item && item.text) return item.text.length;
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make available globally
|
||||||
|
window.TableHelper = TableHelper;
|
||||||
|
|
@ -201,29 +201,24 @@ const boxingDayGame = {
|
||||||
read_messages: {
|
read_messages: {
|
||||||
title: "Private Messages",
|
title: "Private Messages",
|
||||||
content: [
|
content: [
|
||||||
"┌──────────────────────────────────────────────┐",
|
...TableHelper.table({
|
||||||
"│ D A R K T O W E R B B S │",
|
title: "Private Messages for 0BSERVER0",
|
||||||
"├──────────────────────────────────────────────┤",
|
headers: ["#", "FROM", "TO", "DATE", "STATUS"],
|
||||||
"│ Private Mail for: 0BSERVER0 │",
|
rows: [
|
||||||
"├──────────────────────────────────────────────┤",
|
[
|
||||||
"│ # FROM TO DATE STATUS │",
|
"23",
|
||||||
"├──────────────────────────────────────────────┤",
|
"[UNKNOWN]",
|
||||||
{
|
"0BSERVER0",
|
||||||
text: "| 23 [UNKNOWN] 0BSERVER0 25/12 NEW |",
|
"25/12",
|
||||||
className: "warning",
|
{ text: "NEW", className: "warning" },
|
||||||
},
|
],
|
||||||
{
|
["22", "NIGHTWATCHER", "0BSERVER0", "12/12", "READ"],
|
||||||
text: "| 22 NIGHTWAT. 0BSERVER0 12/12 READ |",
|
["21", "0BSERVER0", "NIGHTWATCHER", "11/12", "SENT"],
|
||||||
},
|
["22", "NIGHTWATCHER", "0BSERVER0", "10/12", "READ"],
|
||||||
{
|
],
|
||||||
text: "| 21 0BSERVER0 NIGHTWAT. 11/12 SENT |",
|
widths: [4, 12, 12, 8, 8],
|
||||||
},
|
align: ["right", "left", "left", "left", "left"],
|
||||||
{
|
}),
|
||||||
text: "| 22 NIGHTWAT. 0BSERVER0 10/12 READ |",
|
|
||||||
},
|
|
||||||
"└──────────────────────────────────────────────┘",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
],
|
],
|
||||||
options: [
|
options: [
|
||||||
{ text: "Open unread message", next: "new_message" },
|
{ text: "Open unread message", next: "new_message" },
|
||||||
|
|
@ -308,6 +303,43 @@ const boxingDayGame = {
|
||||||
message_archive: {
|
message_archive: {
|
||||||
title: "Message Archive",
|
title: "Message Archive",
|
||||||
content: [
|
content: [
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
title: "Private Messages for ${username}", // Supports interpolation
|
||||||
|
headers: ["#", "FROM", "TO", "DATE", "STATUS"],
|
||||||
|
rows: [
|
||||||
|
// Simple array row (always shown)
|
||||||
|
["22", "NIGHTWAT.", "0BSERVER0", "12/12", "READ"],
|
||||||
|
|
||||||
|
// Conditional row - only shown if condition is true
|
||||||
|
{
|
||||||
|
condition: { not: "read_new_message" },
|
||||||
|
cells: ["23", "[UNKNOWN]", "0BSERVER0", "25/12", "NEW"],
|
||||||
|
className: "warning", // Applied to all cells in row
|
||||||
|
},
|
||||||
|
|
||||||
|
// Row with per-cell styling
|
||||||
|
{
|
||||||
|
cells: [
|
||||||
|
"21",
|
||||||
|
"0BSERVER0",
|
||||||
|
{ text: "DELETED", className: "error" },
|
||||||
|
"11/12",
|
||||||
|
"SENT",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Conditional with complex logic
|
||||||
|
{
|
||||||
|
condition: { and: ["has_secret", { not: "revealed_secret" }] },
|
||||||
|
cells: ["99", "???", "???", "??/??", "HIDDEN"],
|
||||||
|
className: "error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
widths: [4, 12, 12, 8, 8],
|
||||||
|
align: ["right", "left", "left", "left", "left"],
|
||||||
|
style: "single", // "single", "double", or "ascii"
|
||||||
|
},
|
||||||
"═══ ARCHIVED MESSAGES ═══",
|
"═══ ARCHIVED MESSAGES ═══",
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue