This commit is contained in:
parent
68da6c352e
commit
c61bf64b15
1 changed files with 0 additions and 480 deletions
|
@ -1,480 +0,0 @@
|
|||
/* 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<T> =
|
||||
| MaybeRefOrGetter<T>
|
||||
| ((row: number, col: number, state: State) => T);
|
||||
export type ProcessedCellRefOrGetter<T> =
|
||||
| MaybeRef<T>
|
||||
| ((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<Renderable>;
|
||||
/** 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<number>;
|
||||
/** The number of columns in the grid. */
|
||||
cols: MaybeRefOrGetter<number>;
|
||||
/** A getter for the visibility of a cell. */
|
||||
getVisibility?: CellMaybeRefOrGetter<Visibility | boolean>;
|
||||
/** A getter for if a cell can be clicked. */
|
||||
getCanClick?: CellMaybeRefOrGetter<boolean>;
|
||||
/** A getter for the initial persistent state of a cell. */
|
||||
getStartState: MaybeRefOrGetter<State> | ((row: number, col: number) => State);
|
||||
/** A getter for the CSS styles for a cell. */
|
||||
getStyle?: CellMaybeRefOrGetter<CSSProperties>;
|
||||
/** A getter for the CSS classes for a cell. */
|
||||
getClasses?: CellMaybeRefOrGetter<Record<string, boolean>>;
|
||||
/** 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<Visibility | boolean>;
|
||||
/** A getter for determine if a cell can be clicked. */
|
||||
getCanClick?: ProcessedCellRefOrGetter<boolean>;
|
||||
/** The number of rows in the grid. */
|
||||
rows: MaybeRef<number>;
|
||||
/** The number of columns in the grid. */
|
||||
cols: MaybeRef<number>;
|
||||
/** A getter for the initial persistent state of a cell. */
|
||||
getStartState: MaybeRef<State> | ((row: number, col: number) => State);
|
||||
/** A getter for the CSS styles for a cell. */
|
||||
getStyle?: ProcessedCellRefOrGetter<CSSProperties>;
|
||||
/** A getter for the CSS classes for a cell. */
|
||||
getClasses?: ProcessedCellRefOrGetter<Record<string, boolean>>;
|
||||
/** 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<Record<number, Record<number, State>>>;
|
||||
/** 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<string, Ref<unknown>> = {};
|
||||
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(() => (
|
||||
<Clickable
|
||||
onClick={receiver.onClick}
|
||||
onHold={receiver.onHold}
|
||||
display={receiver.display}
|
||||
canClick={receiver.canClick}
|
||||
/>
|
||||
))
|
||||
];
|
||||
}
|
||||
|
||||
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<T>(
|
||||
value: NonNullable<CellMaybeRefOrGetter<T>>
|
||||
): ProcessedCellRefOrGetter<T>;
|
||||
function convertCellMaybeRefOrGetter<T>(
|
||||
value: CellMaybeRefOrGetter<T> | undefined
|
||||
): ProcessedCellRefOrGetter<T> | undefined;
|
||||
function convertCellMaybeRefOrGetter<T>(
|
||||
value: CellMaybeRefOrGetter<T>
|
||||
): ProcessedCellRefOrGetter<T> {
|
||||
if (typeof value === "function" && value.length > 0) {
|
||||
return value;
|
||||
}
|
||||
return processGetter(value) as MaybeRef<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates a grid with the given options.
|
||||
* @param optionsFunc Grid options.
|
||||
*/
|
||||
export function createGrid<T extends GridOptions>(optionsFunc: () => T) {
|
||||
const cellState = persistent<Record<number, Record<number, State>>>({}, 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<typeof props, keyof VueFeature | keyof GridOptions>),
|
||||
...vueFeatureMixin("grid", options, () => (
|
||||
<Column>
|
||||
{new Array(unref(grid.rows)).fill(0).map((_, row) => (
|
||||
<Row>
|
||||
{new Array(unref(grid.cols))
|
||||
.fill(0)
|
||||
.map((_, col) => render(grid.cells[row][col]))}
|
||||
</Row>
|
||||
))}
|
||||
</Column>
|
||||
)),
|
||||
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;
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue