/** 工具类 主要用来进行一些辅助函数的计算 */
interface Utils {
    /**
     * 四个方向的坐标增量
     */
    readonly scan: DeepReadonly<Scan>;

    /**
     * 八个方向的坐标增量
     */
    readonly scan2: DeepReadonly<Scan2>;

    /**
     * 将一段文字中的${}（表达式）进行替换。很多情况下可以用模板字符串替代
     * @example
     * // 把主角的生命值和持有的黄钥匙数量代入这句话
     * core.replaceText('衬衫的价格是${status:hp}镑${item:yellowKey}便士。');
     * @param text 模板字符串，可以使用${}计算js表达式
     * @param prefix 独立开关前缀
     * @returns 替换完毕后的字符串
     */
    replaceText(text: string, prefix?: string): string;

    /**
     * 对一个表达式中的特殊规则进行替换，如status:xxx等。
     * 其中变量和全局存储会替换中文冒号，其余的不会替换
     * @example
     * // 把这两个冒号表达式替换为core.getStatus('hp')和core.itemCount('yellowKey')这样的函数调用
     * core.replaceValue('衬衫的价格是${status:hp}镑${item:yellowKey}便士。');
     * @param value 模板字符串，注意独立开关不会被替换
     * @returns 替换完毕后的字符串
     */
    replaceValue(value: string): string;

    /**
     * 计算一个表达式的值，支持status:xxx等的计算。
     * @example core.calValue('status:hp + status:def'); // 计算主角的生命值加防御力
     * @param value 待求值的表达式
     * @param prefix 独立开关前缀，一般可省略
     * @returns 求出的值
     */
    calValue(value: string | Function, prefix?: string): any;

    /**
     * @deprecated
     * 将b（可以是另一个数组）插入数组a的开头，用Array.unshift就行
     * @example core.unshift(todo, {type: 'unfollow'}); // 在事件指令数组todo的开头插入“取消所有跟随者”指令
     * @param a 原数组
     * @param b 待插入的新首项或前缀数组
     * @returns 插入完毕后的新数组，它是改变原数组a本身得到的
     */
    unshift<A extends any[], B extends any[]>(a: A, b: B): [...B, ...A];

    /**
     * @deprecated
     * 将b（可以是另一个数组）插入数组a的末尾，用Array.push就行
     * @example core.push(todo, {type: 'unfollow'}); // 在事件指令数组todo的末尾插入“取消所有跟随者”指令
     * @param a 原数组
     * @param b 待插入的新末项或后缀数组
     * @returns 插入完毕后的新数组，它是改变原数组a本身得到的
     */
    push<A extends any[], B extends any[]>(a: A, b: B): [...A, ...B];

    /**
     * 解压缩一个数据，我也不知道这个解压的是什么
     * @param 要解压的内容，字符串
     */
    decompress(value: string): any;

    /**
     * @deprecated
     * 设置本地存储
     * @param key 本地存储的名称
     * @param value 本地存储的值，不填代表删除
     */
    setLocalStorage(key: string, value?: any): void;

    /**
     * @deprecated
     * 获得本地存储
     * @param key 获取的本地存储的名称
     * @param defaultValue 当不存在的时候的默认值
     */
    getLocalStorage<T>(key: string, defaultValue?: T): T;

    /**
     * @deprecated
     * 移除本地存储
     * @param key 要移除的本地存储的值
     */
    removeLocalStorage(key: string): void;

    /**
     * 异步写入localforage
     * @param key 写入的键
     * @param value 写入的值
     * @param successCallback 写入成功的回调函数
     * @param errorCallback 写入出错的回调函数
     */
    setLocalForage(
        key: string,
        value?: any,
        successCallback?: () => void,
        errorCallback?: (err: Error) => void
    ): void;

    /**
     * 从localforage读出一段数据
     */
    getLocalForage<T>(
        key: string,
        defaultValue?: T,
        successCallback?: (data: T) => void,
        errorCallback?: (err: Error) => void
    ): void;

    /**
     * 移除localforage的数据
     */
    removeLocalForage(
        key: string,
        successCallback?: () => void,
        errorCallback?: (err: Error) => void
    ): void;

    /**
     * 清除localforage所有的数据
     * @param callback 清除完毕的回调函数
     */
    clearLocalForage(callback?: (err?: Error) => void): void;

    /**
     * 迭代localforage的数据
     * @param iteratee 迭代器
     * @param callback 迭代完毕的回调函数
     */
    iterateLocalForage<T, U>(
        iteratee: (value: T, key: string, iterationNumber: number) => U,
        callback?: (err: any, result: U) => void
    ): void;

    /**
     * 获取localforage数据的所有的键
     * @param callback 回调函数
     */
    keysLocalForage(callback?: (err: any, keys: string[]) => void): void;

    /**
     * 获取localforage数据的数据量
     * @param callback 回调函数
     */
    lengthLocalForage(
        callback?: (err: any, numberOfKeys: number) => void
    ): void;

    /**
     * @deprecated
     * 设置一个全局存储，适用于global:xxx，录像播放时将忽略此函数。
     * @example core.setBlobal('一周目已通关', true); // 设置全局存储“一周目已通关”为true，方便二周目游戏中的新要素。
     * @param key 全局变量名称，支持中文
     * @param value 全局变量的新值，不填或null表示清除此全局存储
     */
    setGlobal(key: string, value?: any): void;

    /**
     * @deprecated
     * 读取一个全局存储，适用于global:xxx，支持录像。
     * @example if (core.getGlobal('一周目已通关', false) === true) core.getItem('dagger'); // 二周目游戏进行到此处时会获得一把屠龙匕首
     * @param key 全局变量名称，支持中文
     * @param defaultValue 可选，当此全局变量不存在或值为null、undefined时，用此值代替
     * @returns 全局变量的值
     */
    getGlobal<T>(key: string, defaultValue?: T): T;

    /**
     * 深拷贝一个对象(函数将原样返回)
     * @example core.clone(core.status.hero, (name, value) => (name == 'items' || typeof value == 'number'), false); // 深拷贝主角的属性和道具
     * @param data 待拷贝对象
     * @param filter 过滤器，可选，表示data为数组或对象时拷贝哪些项或属性，true表示拷贝
     * @param recursion 过滤器是否递归，可选。true表示过滤器也被递归
     * @returns 拷贝的结果，注意函数将原样返回
     */
    clone<T>(
        data: T,
        filter?: (name: string, value: any) => boolean,
        recursion?: boolean
    ): T;

    /**
     * 深拷贝一个1D或2D的数组
     * @param data 要拷贝的数据
     */
    cloneArray<T extends any[]>(data: T): T;

    /**
     * 等比例切分一张图片
     * @example core.splitImage(core.material.images.images['npc48.png'], 32, 48); // 把npc48.png切分成若干32×48px的小人
     * @param image 图片名（支持映射前的中文名）或图片对象（参见上面的例子），获取不到时返回[]
     * @param width 子图的宽度，单位为像素。原图总宽度必须是其倍数，不填视为32
     * @param height 子图的高度，单位为像素。原图总高度必须是其倍数，不填视为正方形
     * @returns 子图组成的数组，在原图中呈先行后列，从左到右、从上到下排列。
     */
    splitImage(
        image: NameMapIn<ImageIds> | ImageIds | HTMLImageElement,
        width?: number,
        height?: number
    ): HTMLImageElement[];

    /**
     * 格式化日期为字符串
     * @param date 时间，不填代表当前时间
     * @returns 格式: yyyy-mm-dd hh:mm:ss
     */
    formatDate(date?: Date): string;

    /**
     * 格式化日期为最简字符串
     * @param date 时间，不填代表当前时间
     * @returns 格式: yyyymmddhhmmss
     */
    formatDate2(date?: Date): string;

    /**
     * 格式化时间
     * @param time 时间
     * @returns 格式: hh:mm:ss
     */
    formatTime(time: number): string;

    /**
     * @deprecated
     * 设置成两位数显示，请使用setDigits代替
     */
    setTwoDigits(x: number): string;

    /**
     * 设置一个数为n位数显示
     * @param x 要设置的数
     * @param n 设置成的位数
     */
    setDigits(x: number, n: number): string;

    /**
     * 格式化文件大小
     * @param size 大小，字节数
     * @returns 格式为xx.xxB KB MB
     */
    formatSize(size: number): string;

    /**
     * 大数字格式化，单位为10000的倍数（w,e,z,j,g），末尾四舍五入
     * @example core.formatBigNumber(123456789); // "12346w"
     * @param x 原数字
     * @param onMap 显示的字符数
     * @returns 格式化结果
     */
    formatBigNumber<T extends string>(x: T, onMap?: number): T;
    formatBigNumber(x: number | string, onMap?: number | boolean): string;

    /**
     * @deprecated
     * 变速移动，完全可以用mutate-animate代替
     * @param mode 缓动模式
     */
    applyEasing(mode?: EaseMode): (x: number) => number;

    /**
     * 颜色数组转十六进制
     * @example core.arrayToRGB([102, 204, 255]); // "#66ccff"，加载画面的宣传色
     * @param color 一行三列的数组，各元素必须为不大于255的自然数
     * @returns 该颜色的十六进制表示，使用小写字母
     */
    arrayToRGB(color: RGBArray): _RGBA;

    /**
     * 颜色数组转字符串
     * @example core.arrayToRGBA([102, 204, 255]); // "rgba(102,204,255,1)"
     * @param color 一行三列或一行四列的数组，前三个元素必须为不大于255的自然数。
     * 第四个元素（如果有）必须为0或不大于1的数字，第四个元素不填视为1
     * @returns 该颜色的字符串表示
     */
    arrayToRGBA(color: Color): _RGBA;

    /**
     * 录像一压，其结果会被再次base64压缩
     * @example core.encodeRoute(core.status.route); // 一压当前录像
     * @param route 原始录像，自定义内容（不予压缩，原样写入）必须由0-9A-Za-z和下划线、冒号组成，
     * 所以中文和数组需要用JSON.stringify预处理再base64压缩才能交由一压
     * @returns 一压的结果
     */
    encodeRoute(route: string[]): string;

    /**
     * 录像解压的最后一步，即一压的逆过程
     * @example core.decodeRoute(core.encodeRoute(core.status.route)); // 一压当前录像再解压-_-|
     * @param route 录像解压倒数第二步的结果，即一压的结果
     * @returns 原始录像
     */
    decodeRoute(route: string): string[];

    /**
     * 判断一个值是否不为null，undefined和NaN
     * @example core.isset(0/0); // false，因为0/0等于NaN
     * @param v 待测值
     * @returns false表示待测值为null、undefined、NaN或未填写，true表示为其他值
     */
    isset(v?: any): boolean;

    /**
     * 判定一个数组是否为另一个数组的前缀，用于录像接续播放
     * @example core.subarray(['ad', '米库', '小精灵', '小破草', '小艾'], ['ad', '米库', '小精灵']); // ['小破草', '小艾']
     * @param a 可能的母数组，不填或比b短将返回null
     * @param b 可能的前缀，不填或比a长将返回null
     * @returns 如果b不是a的前缀将返回null，否则将返回a去掉此前缀后的剩余数组
     */
    subarray(a: any[], b: any[]): any[] | null;

    /**
     * @deprecated
     * 判定array是不是一个数组，以及element是否在该数组中。使用Array.includes代替
     * @param array 可能的数组，不为数组或不填将导致返回值为false
     * @param element 待查找的元素
     * @returns 如果array为数组且具有element这项，就返回true，否则返回false
     */
    inArray(array?: any, element?: any): boolean;

    /**
     * 将x限定在[a,b]区间内，注意a和b可交换
     * @example core.clamp(1200, 1, 1000); // 1000
     * @param x 原始值，!x为true时x一律视为0
     * @param a 下限值，大于b将导致与b交换
     * @param b 上限值，小于a将导致与a交换
     */
    clamp(x: number, a: number, b: number): number;

    /**
     * 访问浏览器cookie
     */
    getCookie(name: string): string;

    /**
     * @deprecated 已失效（大概
     * 填写非自绘状态栏
     * @example
     * // 更新状态栏中的主角生命，使用加载画面的宣传色
     * core.setStatusBarInnerHTML('hp', core.status.hero.hp, 'color: #66CCFF');
     * @param name 状态栏项的名称，如'hp', 'atk', 'def'等。必须是core.statusBar中的一个合法项
     * @param value 要填写的内容，大数字会被格式化为至多6个字符，无中文的内容会被自动设为斜体
     * @param css 额外的css样式，可选。如更改颜色等
     */
    setStatusBarInnerHTML(
        name: string,
        value: string | number,
        css?: string
    ): void;

    /**
     * 求字符串的国标码字节数，也可用于等宽字体下文本的宽度测算。请注意样板的默认字体Verdana不是等宽字体
     * @example core.strlen('无敌ad'); // 6
     * @param str 待测字符串
     * @returns 字符串的国标码字节数，每个汉字为2，每个ASCII字符为1
     */
    strlen(str: string): number;

    /**
     * 计算应当转向某个方向
     * @param turn 转向的方向
     * @param direction 当前方向，不填视为当前方向
     */
    turnDirection(turn: HeroTurnDir, direction?: Dir): string;

    /**
     * 通配符匹配，用于搜索图块等批量处理。
     * @example core.playSound(core.matchWildcard('*Key', itemId) ? 'item.mp3' : 'door.mp3'); // 判断捡到的是钥匙还是别的道具，从而播放不同的音效
     * @param pattern 模式串，每个星号表示任意多个（0个起）字符
     * @param string 待测串
     * @returns true表示匹配成功，false表示匹配失败
     */
    matchWildcard(pattern: string, string: string): boolean;

    /**
     * 是否满足正则表达式，一般可以直接用/RegExp/.test(str)代替
     * @param pattern 正则表达式
     * @param string 要匹配的字符串
     */
    matchRegex(pattern: string, string: string): string;

    /**
     * base64加密
     * @example
     * core.encodeBase64('If you found this note in a small wooden box with a heart on it');
     * // "SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0"
     * @param str 明文
     * @returns 密文
     */
    encodeBase64(str: string): string;

    /**
     * base64解密
     * @example
     * core.decodeBase64('SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2RlbiBib3ggd2l0aCBhIGhlYXJ0IG9uIGl0');
     * // "If you found this note in a small wooden box with a heart on it"
     * @param str 密文
     * @returns 明文
     */
    decodeBase64(str: string): string;

    /**
     * 不支持SL的随机数
     * @exmaple 1 + core.rand(6); // 随机生成一个小于7的正整数，模拟骰子的效果
     * @param num 填正数表示生成小于num的随机自然数，否则生成小于1的随机正数
     * @returns 随机数，即使读档也不会改变结果
     */
    rand(num?: number): number;

    /**
     * 支持SL的随机数，并计入录像
     * @exmaple 1 + core.rand2(6); // 随机生成一个小于7的正整数，模拟骰子的效果
     * @param num 正整数，0或不填会被视为2147483648
     * @returns 属于 [0, num) 的随机数
     */
    rand2(num?: number): number;

    /**
     * 尝试请求读取一个本地文件内容 [异步]
     * @param success 成功后的回调
     * @param error 失败后的回调
     * @param accept input元素的accept属性
     * @param readType 不设置则以文本读取，否则以DataUrl形式读取
     */
    readFile(
        success: (obj: any) => void,
        error: () => void,
        accept: string,
        readType: boolean
    ): void;

    /**
     * 文件读取完毕后的内容处理 [异步]
     * @param content 读取的内容
     */
    readFileContent(content: string): void;

    /**
     * 弹窗请求下载一个文本文件
     * @example core.download('route.txt', core.status.route); // 弹窗请求下载录像
     * @param filename 文件名
     * @param content 文件内容
     */
    download(filename: string, content: string | string[]): void;

    /**
     * 尝试复制一段文本到剪切板
     * @param data 赋值的东西
     */
    copy(data: string): void;

    /**
     * 显示确认框，类似core.drawConfirmBox()
     * @example core.myconfirm('重启游戏？', core.restart); // 弹窗询问玩家是否重启游戏
     * @param hint 弹窗的内容
     * @param yesCallback 确定后的回调函数
     * @param noCallback 取消后的回调函数
     */
    myconfirm(
        hint: string,
        yesCallback: () => void,
        noCallback?: () => void
    ): void;

    /**
     * 让用户输入一段文字
     */
    myprompt(
        hint: string,
        value: string,
        callback?: (data?: string) => void
    ): void;

    /**
     * @deprecated
     * 动画显示某对象，有vue了，你还用这个？Transition组件和css的transition比这个强得多
     */
    showWithAnimate(
        obj?: HTMLElement,
        speed?: number,
        callback?: () => any
    ): void;

    /**
     * @deprecated
     * 动画使某对象消失
     */
    hideWithAnimate(
        obj?: HTMLElement,
        speed?: number,
        callback?: () => any
    ): void;

    /**
     * 获得浏览器唯一的guid
     */
    getGuid(): string;

    /**
     * 获取一个对象的哈希值
     * @param obj 要获取的对象
     */
    hashCode(obj: any): number;

    /**
     * 判定深层相等, 会逐层比较每个元素
     * @example core.same(['1', 2], ['1', 2]); // true
     */
    same(a: any, b: any): boolean;

    /**
     * 解压一段内容
     */
    unzip(
        blobOrUrl: string | Blob,
        success?: (data: any) => void,
        error?: (error: string) => void,
        convertToText?: boolean,
        onprogress?: (loaded: number, total: number) => void
    ): void;

    /**
     * 发送一个HTTP请求 [异步]
     * @param type 请求类型
     * @param url 目标地址
     * @param formData 如果是POST请求则为表单数据
     * @param success 成功后的回调
     * @param error 失败后的回调
     */
    http(
        type: 'GET' | 'POST',
        url: string,
        formData?: FormData,
        success?: (res: any) => void,
        error?: (err: string) => void,
        mimeType?: string,
        responseType?: XMLHttpRequestResponseType,
        onProgress?: (loaded: number, total: number) => void
    ): void;
}

declare const utils: new () => Utils;

/**
 * APP接口，使用前应先判断是否存在
 */
interface JSInterface {
    /** 强制横屏 */
    requireLandscape(): void;
}

declare const jsinterface: JSInterface;

interface Window {
    readonly jsinterface: JSInterface;
}

/**
 * 移动的方向
 */
type Step = Move | 'backward';

/**
 * 坐标字符串
 */
type LocString = `${number},${number}`;

type _RGBA =
    | `rgb(${number},${number},${number})`
    | `rgba(${number},${number},${number},${number})`;

/**
 * RGBA颜色数组
 */
type RGBArray = [number, number, number, number?];

/**
 * 样板的颜色字符串
 */
type Color = `#${string}` | _RGBA | RGBArray | 'transparent';

/**
 * 四个方向
 */
type Dir = 'up' | 'down' | 'left' | 'right';

/**
 * 八个方向
 */
type Dir2 = Dir | 'leftup' | 'rightup' | 'leftdown' | 'rightdown';

/**
 * 转向的方向
 */
type TurnDir = Dir | ':left' | ':right' | ':back';

/**
 * 勇士转向
 */
type HeroTurnDir = TurnDir | ':hero' | ':backhero';

/**
 * 对话框的位置
 */
type TextPosition = 'up' | 'center' | 'down';

/**
 * 移动的方向
 */
type Move = 'forward' | Dir;

/**
 * 缓动模式，不过在高级动画插件面前不堪一击（
 */
type EaseMode = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';

/**
 * 事件执行的操作符\
 * += 增加并赋值\
 * -= 减少并赋值\
 * *= 相乘并赋值\
 * /= 相除并赋值\
 * //= 除以并取商\
 * **= 取幂\
 * %= 取余\
 * min= 取二者的最小值\
 * max= 取二者的最大值\
 * 其它的任意字符串都是赋值
 */
type MotaOperator =
    | '+='
    | '-='
    | '*='
    | '/='
    | '//='
    | '**='
    | '%='
    | 'min='
    | 'max='
    | '=';

/**
 * 位置数组
 */
type LocArr = [x: number, y: number];

/**
 * 位置
 */
interface Loc {
    /**
     * 横坐标
     */
    x: number;

    /**
     * 纵坐标
     */
    y: number;
}

/**
 * 带方向的位置
 */
interface DiredLoc extends Loc {
    /**
     * 方向
     */
    direction: Dir;
}

interface CompressedStep {
    /**
     * 移动方向
     */
    direction: Dir;

    /**
     * 向该方向移动的步数
     */
    step: number;
}

/**
 * 四个方向的坐标增量
 */
type Scan = {
    [D in Dir]: Loc;
};

/**
 * 八个方向的坐标增量
 */
type Scan2 = {
    [D in Dir2]: Loc;
};

/**
 * 图片翻转
 */
type ImageReverse = ':o' | ':x' | ':y';

/**
 * 对话框的箭头方向
 */
type TextBoxDir = 'up' | 'down';

/**
 * 对话框的位置
 */
type TextBoxPos =
    | `${TextBoxDir},hero`
    | `${TextBoxDir},${number},${number}`
    | TextPosition;

/**
 * 画布信息
 */
type CtxRefer = string | CanvasRenderingContext2D | HTMLCanvasElement;

/**
 * 触发器类型
 */
type MotaTrigger =
    | 'battle'
    | 'pusBox'
    | 'openDoor'
    | 'ski'
    | 'custom'
    | 'getItem'
    | 'changeFloor'
    | 'null';

/**
 * 切换楼层的目标坐标
 */
type FloorChangeStair =
    | 'upFloor'
    | 'downFloor'
    | ':symmetry'
    | ':symmetry_x'
    | ':symmetry_y'
    | 'flyPoint';

/**
 * 事件值的前缀
 */
type EventValuePreffix =
    | 'status'
    | 'flag'
    | 'item'
    | 'buff'
    | 'switch'
    | 'temp'
    | 'global';

interface Animate {
    /**
     * 动画的帧数s
     */
    frame: number;

    /**
     * 每帧的信息
     */
    frames: FrameObj[][];

    /**
     * 图片信息
     */
    images: HTMLImageElement[];

    /**
     * 缩放信息
     */
    ratio: number;

    /**
     * 音效
     */
    se: string;
}

type Save = DeepReadonly<{
    /**
     * 存档所在的楼层id
     */
    floorId: FloorIds;

    /**
     * 存档的勇士信息
     */
    hero: HeroStatus;

    /**
     * 难度信息
     */
    hard: number;

    /**
     * 存档的地图信息，已经过压缩处理
     */
    maps: Record<string, ResolvedFloor>;

    /**
     * 录像信息
     */
    route: string;

    /**
     * 存档的全局变量信息
     */
    values: CoreValues;

    /**
     * 游戏版本
     */
    version: string;

    /**
     * 浏览器唯一guid
     */
    guid: string;

    /**
     * 存档时间
     */
    time: number;
}>;

type ValueType = number | string | boolean | undefined | null | bigint | symbol;

/**
 * 深度只读一个对象，使其所有属性都只读
 */
type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends ValueType ? T[P] : DeepReadonly<T[P]>;
};

/**
 * 深度可选一个对象，使其所有属性都可选
 */
type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends ValueType ? T[P] : DeepPartial<T[P]>;
};

/**
 * 深度必选一个对象，使其所有属性都必选
 */
type DeepRequired<T> = {
    [P in keyof T]-?: T[P] extends ValueType ? T[P] : DeepRequired<T[P]>;
};

/**
 * 使一个对象的所有属性可写
 */
type Writable<T> = {
    -readonly [P in keyof T]: T[P];
};

/**
 * 深度可写一个对象，使其所有属性都可写
 */
type DeepWritable<T> = {
    -readonly [P in keyof T]: T[P] extends ValueType
        ? T[P]
        : DeepWritable<T[P]>;
};

/**
 * 从一个对象中选择类型是目标类型的属性
 */
type SelectType<R, T> = {
    [P in keyof R as R[P] extends T ? P : never]: R[P];
};

/**
 * 从一个对象中选择类型是目标属性的键名
 */
type SelectKey<R, T> = keyof SelectType<R, T>;

/**
 * 获取一段字符串的第一个字符
 */
type FirstCharOf<T extends string> = T extends `${infer F}${infer A}`
    ? F
    : never;

/**
 * 非对象属性
 */
type NonObject = number | string | boolean;

/**
 * 获取一个对象的非对象值
 */
type NonObjectOf<T> = SelectType<T, NonObject>;

/**
 * 以一个字符串结尾
 */
type EndsWith<T extends string> = `${string}${T}`;

type KeyExcludesUnderline<T> = Exclude<keyof T, `_${string}`>;

type ValueOf<T> = T[keyof T];
