Profectus has had a Grid feature to mimic TMT's grid. In 0.7 it got removed due to not really offering a benefit over . If you preferred working with grids though, here's a 0.7 compatible implementation. It has some improvements over the 0.6 grid, like removing the 100 column limit by storing the cell ID as discrete column and row numbers.

/* eslint-disable @typescript-eslint/no-explicit-any */import Column from "components/layout/Column.vue";import Row from "components/layout/Row.vue";import Clickable from "features/clickables/Clickable.vue";import { getUniqueID, Visibility } from "features/feature";import type { Persistent, State } from "game/persistence";import { persistent } from "game/persistence";import { isFunction } from "util/common";import { MaybeGetter, processGetter } from "util/computed";import { createLazyProxy } from "util/proxies";import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions} from "util/vue";import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";import { computed, unref } from "vue";/** A symbol used to identify {@link Grid} features. */export const GridType = Symbol("Grid");/** A type representing a MaybeRefOrGetter value for a cell in the grid. */export type CellMaybeRefOrGetter = | MaybeRefOrGetter | ((row: number, col: number, state: State) => T);export type ProcessedCellRefOrGetter = | MaybeRef | ((row: number, col: number, state: State) => T);/** * Represents a cell within a grid. These properties will typically be accessed via a cell proxy that calls functions on the grid to get the properties for a specific cell. * @see {@link createGridProxy} */export interface GridCell extends VueFeature { /** Which roe in the grid this cell is from. */ row: number; /** Which col in the grid this cell is from. */ col: number; /** Whether this cell can be clicked. */ canClick: boolean; /** The initial persistent state of this cell. */ startState: State; /** The persistent state of this cell. */ state: State; /** The main text that appears in the display. */ display: MaybeGetter; /** A function that is called when the cell is clicked. */ onClick?: (e?: MouseEvent | TouchEvent) => void; /** A function that is called when the cell is held down. */ onHold?: VoidFunction;}/** * An object that configures a {@link Grid}. */export interface GridOptions extends VueFeatureOptions { /** The number of rows in the grid. */ rows: MaybeRefOrGetter; /** The number of columns in the grid. */ cols: MaybeRefOrGetter; /** A getter for the visibility of a cell. */ getVisibility?: CellMaybeRefOrGetter; /** A getter for if a cell can be clicked. */ getCanClick?: CellMaybeRefOrGetter; /** A getter for the initial persistent state of a cell. */ getStartState: MaybeRefOrGetter | ((row: number, col: number) => State); /** A getter for the CSS styles for a cell. */ getStyle?: CellMaybeRefOrGetter; /** A getter for the CSS classes for a cell. */ getClasses?: CellMaybeRefOrGetter>; /** A getter for the display component for a cell. */ getDisplay: | Renderable | ((row: number, col: number, state: State) => Renderable) | { getTitle?: Renderable | ((row: number, col: number, state: State) => Renderable); getDescription: Renderable | ((row: number, col: number, state: State) => Renderable); }; /** A function that is called when a cell is clicked. */ onClick?: (row: number, col: number, state: State, e?: MouseEvent | TouchEvent) => void; /** A function that is called when a cell is held down. */ onHold?: (row: number, col: number, state: State) => void;}/** An object that represents a feature that is a grid of cells that all behave according to the same rules. */export interface Grid extends VueFeature { /** A function that is called when a cell is clicked. */ onClick?: (row: number, col: number, state: State, e?: MouseEvent | TouchEvent) => void; /** A function that is called when a cell is held down. */ onHold?: (row: number, col: number, state: State) => void; /** A getter for determine the visibility of a cell. */ getVisibility?: ProcessedCellRefOrGetter; /** A getter for determine if a cell can be clicked. */ getCanClick?: ProcessedCellRefOrGetter; /** The number of rows in the grid. */ rows: MaybeRef; /** The number of columns in the grid. */ cols: MaybeRef; /** A getter for the initial persistent state of a cell. */ getStartState: MaybeRef | ((row: number, col: number) => State); /** A getter for the CSS styles for a cell. */ getStyle?: ProcessedCellRefOrGetter; /** A getter for the CSS classes for a cell. */ getClasses?: ProcessedCellRefOrGetter>; /** A getter for the display component for a cell. */ getDisplay: Renderable | ((row: number, col: number, state: State) => Renderable); /** Get the auto-generated ID for identifying a specific cell of this grid that appears in the DOM. Will not persist between refreshes or updates. */ getID: (row: number, col: number, state: State) => string; /** Get the persistent state of the given cell. */ getState: (row: number, col: number) => State; /** Set the persistent state of the given cell. */ setState: (row: number, col: number, state: State) => void; /** A dictionary of cells within this grid. */ cells: GridCell[][]; /** The persistent state of this grid, which is a dictionary of cell states. */ cellState: Persistent>>; /** A symbol that helps identify features of the same type. */ type: typeof GridType;}function getCellRowHandler(grid: Grid, row: number) { return new Proxy({} as GridCell[], { get(target, key) { if (key === "length") { return unref(grid.cols); } if (typeof key !== "number" && typeof key !== "string") { return; } const keyNum = typeof key === "number" ? key : parseInt(key); if (Number.isFinite(keyNum) && keyNum < unref(grid.cols)) { if (keyNum in target) { return target[keyNum]; } return (target[keyNum] = getCellHandler(grid, row, keyNum)); } }, set(target, key, value) { console.warn("Cannot set grid cells", target, key, value); return false; }, ownKeys() { return [...new Array(unref(grid.cols)).fill(0).map((_, i) => "" + i), "length"]; }, has(target, key) { if (key === "length") { return true; } if (typeof key !== "number" && typeof key !== "string") { return false; } const keyNum = typeof key === "number" ? key : parseInt(key); if (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols)) { return false; } return true; }, getOwnPropertyDescriptor(target, key) { if (typeof key !== "number" && typeof key !== "string") { return; } const keyNum = typeof key === "number" ? key : parseInt(key); if (key !== "length" && (!Number.isFinite(keyNum) || keyNum >= unref(grid.cols))) { return; } return { configurable: true, enumerable: true, writable: false }; } });}/** * Returns traps for a proxy that will get the properties for the specified cell * @param id The grid cell ID to get properties from. * @see {@link getGridHandler} * @see {@link createGridProxy} */function getCellHandler(grid: Grid, row: number, col: number): GridCell { const keys = [ "id", "visibility", "classes", "style", "components", "wrappers", VueFeature, "row", "col", "canClick", "startState", "state", "title", "display", "onClick", "onHold" ] as const; const cache: Record> = {}; return new Proxy({} as GridCell, { // The typing in this function is absolutely atrocious in order to support custom properties get(target, key, receiver) { switch (key) { case "wrappers": return []; case VueFeature: return true; case "row": return row; case "col": return col; case "startState": { if (typeof grid.getStartState === "function") { return grid.getStartState(row, col); } return unref(grid.getStartState); } case "state": { return grid.getState(row, col); } case "id": return (target.id = target.id ?? getUniqueID("gridcell")); case "components": return [ computed(() => (  )) ]; } if (typeof key === "symbol") { return (grid as any)[key]; } key = key.slice(0, 1).toUpperCase() + key.slice(1); let prop = (grid as any)[`get${key}`]; if (isFunction(prop)) { if (!(key in cache)) { cache[key] = computed(() => prop.call(receiver, row, col, grid.getState(row, col)) ); } return cache[key].value; } else if (prop != null) { return unref(prop); } prop = (grid as any)[`on${key}`]; if (isFunction(prop)) { return () => prop.call(receiver, row, col, grid.getState(row, col)); } else if (prop != null) { return prop; } // Revert key change key = key.slice(0, 1).toLowerCase() + key.slice(1); prop = (grid as any)[key]; if (isFunction(prop)) { return () => prop.call(receiver, row, col, grid.getState(row, col)); } return (grid as any)[key]; }, set(target, key, value) { if (typeof key !== "string") { return false; } key = `set${key.slice(0, 1).toUpperCase() + key.slice(1)}`; if (key in grid && isFunction((grid as any)[key]) && (grid as any)[key].length <= 3) { (grid as any)[key].call(grid, row, col, value); return true; } else { console.warn(`No setter for "${key}".`, target); return false; } }, ownKeys() { return keys; }, has(target, key) { return (keys as readonly (string | symbol)[]).includes(key); }, getOwnPropertyDescriptor(target, key) { if ((keys as readonly (string | symbol)[]).includes(key)) { return { configurable: true, enumerable: true, writable: false }; } } });}function convertCellMaybeRefOrGetter( value: NonNullable>): ProcessedCellRefOrGetter;function convertCellMaybeRefOrGetter( value: CellMaybeRefOrGetter | undefined): ProcessedCellRefOrGetter | undefined;function convertCellMaybeRefOrGetter( value: CellMaybeRefOrGetter): ProcessedCellRefOrGetter { if (typeof value === "function" && value.length > 0) { return value; } return processGetter(value) as MaybeRef;}/** * Lazily creates a grid with the given options. * @param optionsFunc Grid options. */export function createGrid(optionsFunc: () => T) { const cellState = persistent>>({}, false); return createLazyProxy(() => { const options = optionsFunc(); const { rows, cols, getVisibility, getCanClick, getStartState, getStyle, getClasses, getDisplay: _getDisplay, onClick, onHold, ...props } = options; let getDisplay; if (typeof _getDisplay === "object" && !isJSXElement(_getDisplay)) { const { getTitle, getDescription } = _getDisplay; getDisplay = function (row: number, col: number, state: State) { const title = typeof getTitle === "function" ? getTitle(row, col, state) : getTitle; const description = typeof getDescription === "function" ? getDescription(row, col, state) : getDescription; return ( <> {title} {description} ); }; } else { getDisplay = _getDisplay; } const grid = { type: GridType, ...(props as Omit), ...vueFeatureMixin("grid", options, () => (  {new Array(unref(grid.rows)).fill(0).map((_, row) => (  {new Array(unref(grid.cols)) .fill(0) .map((_, col) => render(grid.cells[row][col]))}  ))}  )), cellState, cells: new Proxy({} as GridCell[][], { get(target, key: PropertyKey) { if (key === "length") { return unref(grid.rows); } if (typeof key !== "number" && typeof key !== "string") { return; } const keyNum = typeof key === "number" ? key : parseInt(key); if (Number.isFinite(keyNum) && keyNum < unref(grid.rows)) { if (!(keyNum in target)) { target[keyNum] = getCellRowHandler(grid, keyNum); } return target[keyNum]; } }, set(target, key, value) { console.warn("Cannot set grid cells", target, key, value); return false; }, ownKeys(): string[] { return [...new Array(unref(grid.rows)).fill(0).map((_, i) => "" + i), "length"]; }, has(target, key) { if (key === "length") { return true; } if (typeof key !== "number" && typeof key !== "string") { return false; } const keyNum = typeof key === "number" ? key : parseInt(key); if (!Number.isFinite(keyNum) || keyNum >= unref(grid.rows)) { return false; } return true; }, getOwnPropertyDescriptor(target, key) { if (typeof key !== "number" && typeof key !== "string") { return; } const keyNum = typeof key === "number" ? key : parseInt(key); if ( key !== "length" && (!Number.isFinite(keyNum) || keyNum >= unref(grid.rows)) ) { return; } return { configurable: true, enumerable: true, writable: false }; } }), rows: processGetter(rows), cols: processGetter(cols), getVisibility: convertCellMaybeRefOrGetter(getVisibility ?? true), getCanClick: convertCellMaybeRefOrGetter(getCanClick ?? true), getStartState: typeof getStartState === "function" && getStartState.length > 0 ? getStartState : processGetter(getStartState), getStyle: convertCellMaybeRefOrGetter(getStyle), getClasses: convertCellMaybeRefOrGetter(getClasses), getDisplay, getID: function (row: number, col: number): string { return grid.id + "-" + row + "-" + col; }, getState: function (row: number, col: number): State { cellState.value[row] ??= {}; if (cellState.value[row][col] != null) { return cellState.value[row][col]; } return grid.cells[row][col].startState; }, setState: function (row: number, col: number, state: State) { cellState.value[row] ??= {}; cellState.value[row][col] = state; }, onClick: onClick == null ? undefined : function (row, col, state, e) { if (grid.cells[row][col].canClick !== false) { onClick.call(grid, row, col, state, e); } }, onHold: onHold == null ? undefined : function (row, col, state) { if (grid.cells[row][col].canClick !== false) { onHold.call(grid, row, col, state); } } } satisfies Grid; return grid; });}


Discuss this on our forum.