工具函数
Estimated reading time: 10 minutes
工具函数是框架提供给模组脚本共用的一组基础 API,用来减少重复代码。它们覆盖几类常见需求:
- 克隆、比较、合并对象或数组。
- 判断数组、对象、
Set、Map、字符串中是否包含某些内容。
- 从数组中随机取值,或按权重取值。
- 转换字符串命名格式。
- 限制数字范围。
- 检查图片资源。
- 处理文本、JSON、字节、Base64 和路径。
框架初始化时会安装一部分原型/静态方法,同时也会把常用工具挂到全局,方便模组脚本直接使用。
推荐写法
对已有值操作时,使用原型方法:
const copy = source.clone();
const same = oldData.equal(newData);
const ok = tags.contains('beast');
const key = 'My Text'.convert('snake');
创建新对象或新数组时,使用静态方法:
const options = Object.merge(defaultOptions, userOptions);
const list = Array.append(baseList, modList);
数值工具放在 Math 上:
const value = Math.clamp(input, 0, 100);
原型方法与静态方法
原型方法会修改调用者本身:
target.merge(source);
target.append(source);
target.cover(source);
静态方法会创建一个新的对象或数组:
const next = Object.merge(defaults, current);
const list = Array.append(base, extra);
如果你不想影响原数据,优先使用 Object.merge()、Object.append()、Object.cover()、Array.merge()、Array.append()、Array.cover()。
常用方法总览
clone
const deepCopy = source.clone();
const shallowCopy = source.clone(false);
const plainCopy = source.clone(true, false);
参数:
支持普通对象、数组、Date、RegExp、Map、Set、ArrayBuffer、DataView 和 TypedArray。
clone() 会处理循环引用。不可枚举属性不会被复制。
equal
const same = dataA.equal(dataB);
equal() 使用深度比较,适合比较对象、数组、嵌套结构。它比 === 更适合判断配置内容是否一致。
merge / append / cover
这三个方法都会递归合并对象,区别主要在数组处理方式。
({ list: [1, 2] }).merge({ list: [3] }); // { list: [3, 2] }
({ list: [1, 2] }).append({ list: [3] }); // { list: [1, 2, 3] }
({ list: [1, 2] }).cover({ list: [3] }); // { list: [3] }
对象会递归合并:
const result = Object.merge(
{ npc: { enabled: true, count: 2 } },
{ npc: { count: 4 } }
);
// { npc: { enabled: true, count: 4 } }
多个来源会按顺序合并,后面的来源优先:
const options = Object.merge(defaults, modDefaults, playerOptions);
mergefn / appendfn / coverfn
过滤版本会在每个字段合并前调用过滤函数。
target.mergefn((key, value, depth, targetValue) => targetValue === undefined, source);
过滤函数参数:
常见用法:
// 只写入目标中没有的字段
target.mergefn((_key, _value, _depth, targetValue) => targetValue === undefined, source);
// 只合并前两层
target.mergefn((_key, _value, depth) => depth <= 2, source);
// 跳过 null / undefined
target.mergefn((_key, value) => value != null, source);
contains
数组:
[1, 2, 3].contains(2); // true
[1, 2, 3].contains([1, 2], 'all'); // true
[1, 2, 3].contains([2, 4], 'any'); // true
[1, 2, 3].contains([4, 5], 'none'); // true
对象、Set、Map 会检查它们的值:
({ a: 1, b: 2 }).contains(2); // true
new Set(['a', 'b']).contains('a'); // true
new Map([['key', 'value']]).contains('value'); // true
字符串:
'Hello World'.contains('World'); // true
'Hello World'.contains('hello', { case: false }); // true
mode 可选:
options 可选:
示例:
const list = [{ id: 1 }, { id: 2 }];
list.contains({ id: 1 }, 'any', { deep: true }); // true
list.contains(2, 'any', { compare: (item, value) => item.id === value }); // true
random / either
Math.random() 保留原生无参数行为:
Math.random(); // 0 到 1 的浮点数
带参数时使用框架扩展:
Math.random(10); // 0 到 10 的整数
Math.random(5, 10); // 5 到 10 的整数
Math.random(5, 10, true); // 5 到 10 的浮点数
数组随机:
['a', 'b', 'c'].random();
按权重选择:
['rare', 'normal'].either([0.1, 0.9]);
允许返回 null:
['a', 'b'].either(undefined, true);
需要可复现随机序列时,使用 随机数系统。
convert
'Hello World'.convert('snake'); // hello_world
'Hello World'.convert('kebab'); // hello-world
'hello world'.convert('pascal'); // HelloWorld
'hello world'.convert('camel'); // helloWorld
支持模式:
可选参数:
'NPC name'.convert('title'); // NPC Name
'NPC name'.convert('title', { acronym: false }); // Npc Name
Math.clamp
Math.clamp('12.5', 0, 100); // 12.5
Math.clamp(120, 0, 100); // 100
Math.clamp(undefined, 0, 100, 10); // 10
fallback 只在输入无法转换成有限数字时使用;不传时使用较小边界值。
min 和 max 可以反过来传,框架会自动取正确区间:
Math.clamp(5, 10, 0); // 5
loadImage
const result = await loadImage('img/myMod/icon.png');
if (result) {
console.log('图片可用');
}
loadImage() 会优先通过 ModLoader 读取图片。如果读取失败,会尝试检查路径本身是否可用。
返回值可能是:
异步场景建议始终使用 await。
字节与 Base64 工具
这些函数适合云存档、导入导出、压缩数据、网络请求等场景。
const bytes = textToBytes('hello');
const text = bytesToText(bytes);
const jsonBytes = jsonToBytes({ ok: true });
const data = bytesToJson(jsonBytes);
const base64 = bytesToBase64(bytes);
const bytesAgain = base64ToBytes(base64);
const buffer = base64ToArrayBuffer(base64);
路径与文本工具
trimSlashes('/a/b/'); // a/b
joinPath('/cloud/', '/slot/', '1'); // cloud/slot/1
joinEncodedPath('user name', 'slot 1'); // user%20name/slot%201
escapeHtmlText('<b>text</b>'); // <b>text</b>
widgets() 用于清理 .twee 文件导入后的外层 passage 声明。
import Options from '@/twee/Options.twee';
const content = widgets(Options);
传入多个内容时返回数组:
const list = widgets(Options, Cheats);
SelectCase
SelectCase 适合把一组条件和结果写成链式结构。
const result = new SelectCase()
.case('wolf', '狼')
.caseIn(['cat', 'dog'], '动物')
.caseRange(0, 10, '低')
.caseIncludes('NPC', '角色')
.caseRegex(/^mod:/, '模组')
.else('未知')
.match(value);
常用方法:
全局函数
常用工具也会挂到全局,适合旧脚本或简单场景:
clone(source);
equal(a, b);
merge(target, source);
append(target, source);
cover(target, source);
contains(list, value);
random(1, 10);
either(list);
convert('Hello World', 'snake');
clamp(value, 0, 100);
loadImage(src);
新代码更推荐原型/静态写法,因为阅读时更容易看出"谁是被操作的数据"。