"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; (function () { "use strict"; let ParameterType; (function (ParameterType) { ParameterType["String"] = "string"; ParameterType["Number"] = "number"; ParameterType["Boolean"] = "boolean"; ParameterType["Enum"] = "enum"; })(ParameterType || (ParameterType = {})); let MethodType; (function (MethodType) { MethodType["Mod"] = "mod"; MethodType["Surgery"] = "surgery"; MethodType["Comment"] = "comment"; MethodType["CustomMethod"] = "custom_method"; MethodType["Ink"] = "ink"; MethodType["Variable"] = "variable"; MethodType["Condition"] = "condition"; MethodType["CustomClass"] = "custom_class"; })(MethodType || (MethodType = {})); const enumTypeMap = { Characters: ["Logan", "Green", "Blue", "Yellow", "Purple", "Orange"], Size: ["Small", "Medium", "Large"], Shape: ["Circle", "Square", "Triangle"], }; class NumericRange { constructor(min, max, isInteger = false) { this.min = min; this.max = max; this.isInteger = isInteger; } } class Parameter { constructor(name, type, description, enumType, numericRange) { this.name = name; this.type = type; this.description = description; this.enumType = enumType; this.numericRange = numericRange; } } class Method { constructor(name, type, description) { this.name = name; this.type = type; this.description = description; } } class Mod extends Method { constructor(name, description) { super(name, MethodType.Mod, description); } } class Surgery extends Method { constructor(name, description) { super(name, MethodType.Surgery, description); } } class CommentMethod extends Method { constructor(name, description, parameters) { super(name, MethodType.Comment, description); this.parameters = parameters; } } class InkMethod extends Method { constructor(name, description, parameters) { super(name, MethodType.Ink, description); this.parameters = parameters; } } class CustomMethods extends Method { constructor(name, description, parameters) { super(name, MethodType.CustomMethod, description); this.parameters = parameters; } } class VariableMethod extends Method { constructor(name, description, parameters) { super(name, MethodType.Variable, description); this.parameters = parameters; } } class ConditionMethod extends Method { constructor(name, description, parameters) { super(name, MethodType.Condition, description); this.parameters = parameters; } } class CustomClassMethod extends Method { constructor(name, description) { super(name, MethodType.CustomClass, description); } } function editDistance(a, b, caseSensitive = true) { if (a.length === 0) return b.length; if (b.length === 0) return a.length; const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { const ac = a.charAt(j - 1); const bc = b.charAt(i - 1); if (caseSensitive ? bc === ac : bc.toLowerCase() === ac.toLowerCase()) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, Math.min(matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)); } } } return matrix[b.length][a.length]; } function parametersOf(method) { var _a; return (_a = method.parameters) !== null && _a !== void 0 ? _a : []; } // --------------------------------------------------------------------- // 数据:可直接在此编辑,或通过 window.CustomMethodSearch.setMethods(...) 覆盖 // --------------------------------------------------------------------- let methods = [ new Mod("wobblyLines", "让单发拍子的轨道抖动"), new Mod("invisibleHeart", "隐藏轨道上的心脏"), new CustomMethods("ChangeCharacter", "修改某条轨道对应的人物", [ new Parameter("character", ParameterType.Enum, "Characters"), new Parameter("row", ParameterType.Number, "轨道编号", undefined, new NumericRange(0, 15, true)), ]), new CustomMethods("DisableRowReflections", "开关某个房间的地面反射", [ new Parameter("room", ParameterType.Number, "房间编号", undefined, new NumericRange(0, 3, true)), ]), new CustomMethods("SetRowLength", "设置轨道长度", [ new Parameter("row", ParameterType.Number, "轨道编号", undefined, new NumericRange(0, 15, true)), new Parameter("length", ParameterType.Number, "轨道长度", undefined, new NumericRange(1, 7, true)), ]), new CommentMethod("trueCameraMove", "移动真实摄像头", [ new Parameter("x", ParameterType.Number, "目标 X"), new Parameter("y", ParameterType.Number, "目标 Y"), new Parameter("angle", ParameterType.Number, "角度"), new Parameter("duration", ParameterType.Number, "时长"), new Parameter("ease", ParameterType.Enum, "缓动类型", "EaseType"), ]), new CustomMethods("DemoAllTypes", "演示四种参数控件", [ new Parameter("text", ParameterType.String, "文本"), new Parameter("count", ParameterType.Number, "数字"), new Parameter("enabled", ParameterType.Boolean, "开关"), new Parameter("character", ParameterType.Enum, "角色", "Characters"), new Parameter("color", ParameterType.Enum, "颜色"), ]), ]; // 合并外置数据(script/custommethod-data.js),同名以内置为准 function hydrateMethod(raw) { var _a, _b, _c, _d, _e, _f, _g, _h; const parameters = Array.isArray(raw.parameters) ? raw.parameters.map((item) => { var _a, _b; return new Parameter(item.name, (_a = item.type) !== null && _a !== void 0 ? _a : ParameterType.String, (_b = item.description) !== null && _b !== void 0 ? _b : "", item.enumType, item.numericRange ? new NumericRange(item.numericRange.min, item.numericRange.max, item.numericRange.isInteger) : undefined); }) : []; switch (raw.type) { case MethodType.Mod: return new Mod(raw.name, (_a = raw.description) !== null && _a !== void 0 ? _a : ""); case MethodType.Surgery: return new Surgery(raw.name, (_b = raw.description) !== null && _b !== void 0 ? _b : ""); case MethodType.Comment: return new CommentMethod(raw.name, (_c = raw.description) !== null && _c !== void 0 ? _c : "", parameters); case MethodType.Ink: return new InkMethod(raw.name, (_d = raw.description) !== null && _d !== void 0 ? _d : "", parameters); case MethodType.Variable: return new VariableMethod(raw.name, (_e = raw.description) !== null && _e !== void 0 ? _e : "", parameters); case MethodType.Condition: return new ConditionMethod(raw.name, (_f = raw.description) !== null && _f !== void 0 ? _f : "", parameters); case MethodType.CustomClass: return new CustomClassMethod(raw.name, (_g = raw.description) !== null && _g !== void 0 ? _g : ""); default: return new CustomMethods(raw.name, (_h = raw.description) !== null && _h !== void 0 ? _h : "", parameters); } } function loadExternalData() { const data = window.CustomMethodSearchData; if (!data) return; if (data.enumTypes) { for (const key of Object.keys(data.enumTypes)) { enumTypeMap[key] = data.enumTypes[key]; } } if (Array.isArray(data.methods)) { const index = new Map(); methods.forEach((method, i) => index.set(method.name.toLowerCase(), i)); for (const raw of data.methods) { if (!raw || !raw.name) continue; const name = String(raw.name).toLowerCase(); const hydrated = hydrateMethod(raw); if (index.has(name)) { methods[index.get(name)] = hydrated; } else { index.set(name, methods.length); methods.push(hydrated); } } } } loadExternalData(); // --------------------------------------------------------------------- // 匹配 // --------------------------------------------------------------------- function searchMethods(query) { const q = query.trim(); if (!q) return []; const ql = q.toLowerCase(); const maxDistance = Math.max(2, Math.floor(q.length / 2)); return methods .map((method) => { const name = method.name; const lower = name.toLowerCase(); let score; if (name === q) { score = 0; } else if (lower.startsWith(ql)) { score = 1; } else if (lower.indexOf(ql) >= 0) { score = 2; } else { score = 3 + editDistance(name, q, false); } return { method, score }; }) .filter((item) => item.score <= 2 || item.score <= 3 + maxDistance) .sort((a, b) => a.score - b.score || a.method.name.localeCompare(b.method.name)) .slice(0, 8) .map((item) => item.method); } // --------------------------------------------------------------------- // DOM 构建 // --------------------------------------------------------------------- function el(tag, className) { const node = document.createElement(tag); if (className) node.className = className; return node; } function resolveEnumOptions(parameter) { if (parameter.enumType && enumTypeMap[parameter.enumType]) { return enumTypeMap[parameter.enumType]; } // 兼容把枚举类型名写在 description 的情况 if (enumTypeMap[parameter.description]) { return enumTypeMap[parameter.description]; } if (enumTypeMap[parameter.name]) { return enumTypeMap[parameter.name]; } return []; } // 布尔值可用 true/false,也可用布尔变量 b0~b9 const BOOLEAN_OPTIONS = ["true", "false"]; for (let i = 0; i < 10; i++) { BOOLEAN_OPTIONS.push("b" + i); } function isNumberVariable(value) { // 数字位置允许的变量:i0~i9(整型)、f0~f9(浮点)、b0~b9(布尔) return /^[ifb][0-9]$/.test(value); } function makeCombo(parameter, options, onChange) { const combo = el("span", "cm-combo"); const input = el("input", "cm-input cm-combo-input"); input.type = "text"; input.placeholder = parameter.name; input.autocomplete = "off"; const list = el("ul", "cm-combo-list"); list.hidden = true; function populate() { list.textContent = ""; if (options.length === 0) { list.hidden = true; return; } const query = input.value.trim(); const ql = query.toLowerCase(); const maxDistance = Math.max(1, Math.floor(query.length / 2)); const shown = (query ? options.filter((option) => option.toLowerCase().indexOf(ql) >= 0 || editDistance(option, query, false) <= maxDistance) : options).slice(0, 10); if (shown.length === 0) { list.hidden = true; return; } for (const option of shown) { const item = el("li", "cm-combo-item"); item.textContent = option; item.title = option; item.addEventListener("mousedown", (event) => { event.preventDefault(); input.value = option; list.hidden = true; onChange(); }); list.appendChild(item); } list.hidden = false; } input.addEventListener("input", () => { populate(); onChange(); }); input.addEventListener("focus", populate); input.addEventListener("blur", () => { window.setTimeout(() => { list.hidden = true; }, 120); }); combo.appendChild(input); combo.appendChild(list); return combo; } function makeControl(parameter, onChange) { switch (parameter.type) { case ParameterType.Boolean: { return makeCombo(parameter, BOOLEAN_OPTIONS, onChange); } case ParameterType.Enum: { // 可输入的下拉框:输入时用编辑距离筛选候选项 return makeCombo(parameter, resolveEnumOptions(parameter), onChange); } case ParameterType.Number: { // 普通输入框,合法性由 readParameter 校验(非数字标红) const number = el("input", "cm-input"); number.type = "text"; number.inputMode = "decimal"; number.placeholder = parameter.name; number.addEventListener("input", onChange); return number; } default: { const text = el("input", "cm-input"); text.type = "text"; text.placeholder = parameter.name; text.addEventListener("input", onChange); return text; } } } function readParameter(parameter, control) { switch (parameter.type) { case ParameterType.Boolean: { const input = control.querySelector("input"); const value = input.value.trim(); const valid = value === "true" || value === "false" || /^b[0-9]$/.test(value); return { text: value || "<布尔>", valid, invalid: value.length > 0 && !valid, control }; } case ParameterType.Enum: { const input = control.querySelector("input"); const value = input.value.trim(); const options = resolveEnumOptions(parameter); const valid = options.length > 0 ? options.some((option) => option.toLowerCase() === value.toLowerCase()) : value.length > 0; return { text: value || "<选项>", valid, invalid: value.length > 0 && !valid, control }; } case ParameterType.Number: { const input = control; const value = input.value.trim(); if (value.length === 0) { return { text: "<数字>", valid: false, invalid: false, control }; } if (isNumberVariable(value)) { return { text: value, valid: true, invalid: false, control }; } const number = Number(value); let valid = isFinite(number); const range = parameter.numericRange; if (valid && range) { if (range.isInteger && !Number.isInteger(number)) valid = false; if (number < range.min || number > range.max) valid = false; } return { text: value, valid, invalid: !valid, control }; } default: { const input = control; const value = input.value.trim(); return { text: value ? `str:${value}` : "<文本>", valid: value.length > 0, invalid: false, control }; } } } function stripStringPrefix(text) { return text.startsWith("str:") ? text.slice(4) : text; } function formatMethod(method, states) { const values = states.map((state) => state.text); const args = values.join(", "); switch (method.type) { case MethodType.Comment: return `()=>${method.name}(${args})`; case MethodType.Ink: return `DOTHIS{${method.name}(${args})}`; case MethodType.Variable: return `${method.name} = ${values.length ? stripStringPrefix(values[0]) : ""}`; case MethodType.Mod: return `"mods": [..., "${method.name}"]`; case MethodType.CustomClass: return `"customClass": "${method.name}"`; case MethodType.Condition: return values.length ? `${method.name}(${args})` : method.name; case MethodType.Surgery: return method.name; default: return values.length ? `${method.name}(${args})` : method.name; } } function copyText(text) { return __awaiter(this, void 0, void 0, function* () { try { if (navigator.clipboard && navigator.clipboard.writeText) { yield navigator.clipboard.writeText(text); return true; } } catch (error) { // fallthrough to execCommand } try { const area = document.createElement("textarea"); area.value = text; area.style.position = "fixed"; area.style.opacity = "0"; document.body.appendChild(area); area.select(); const ok = document.execCommand("copy"); document.body.removeChild(area); return ok; } catch (error) { return false; } }); } function renderContainer(container) { if (container.dataset.cmReady === "1") return; container.dataset.cmReady = "1"; // 优先使用 custommethod.md 里写好的骨架;缺失时再动态生成 let query = container.querySelector(".cm-query"); let suggest = container.querySelector(".cm-suggest"); let selected = container.querySelector(".cm-selected"); let nameEl = container.querySelector(".cm-name"); let descEl = container.querySelector(".cm-desc"); let row = container.querySelector(".cm-row"); let preview = container.querySelector(".cm-preview"); if (!query || !suggest || !selected || !nameEl || !descEl || !row || !preview) { container.textContent = ""; const searchBar = el("div", "cm-search"); query = el("input", "cm-query"); query.type = "text"; query.placeholder = "输入自定义方法名,如 ChangeCharacter"; query.autocomplete = "off"; suggest = el("ul", "cm-suggest"); searchBar.appendChild(query); searchBar.appendChild(suggest); selected = el("div", "cm-selected"); selected.hidden = true; const head = el("div", "cm-head"); nameEl = el("span", "cm-name"); descEl = el("span", "cm-desc"); head.appendChild(nameEl); head.appendChild(descEl); row = el("div", "cm-row"); preview = el("code", "cm-preview"); selected.appendChild(head); selected.appendChild(row); selected.appendChild(preview); container.appendChild(searchBar); container.appendChild(selected); } const queryEl = query; const suggestEl = suggest; const selectedEl = selected; const nameNode = nameEl; const descNode = descEl; const rowEl = row; const previewEl = preview; const copyBtn = el("button", "cm-copy"); copyBtn.type = "button"; copyBtn.textContent = "复制"; copyBtn.hidden = true; copyBtn.title = "复制生成的调用代码"; selectedEl.appendChild(copyBtn); copyBtn.addEventListener("click", () => { copyText(previewEl.textContent).then((ok) => { copyBtn.textContent = ok ? "已复制" : "复制失败"; window.setTimeout(() => { copyBtn.textContent = "复制"; }, 1200); }); }); function clearSuggest() { suggestEl.textContent = ""; } function selectMethod(method) { clearSuggest(); queryEl.value = method.name; selectedEl.hidden = false; nameNode.textContent = method.name; descNode.textContent = method.description; rowEl.textContent = ""; previewEl.textContent = ""; copyBtn.hidden = true; const parameters = parametersOf(method); const controls = []; function updatePreview() { const states = controls.map((item) => readParameter(item.parameter, item.control)); states.forEach((state) => state.control.classList.toggle("cm-invalid", state.invalid)); previewEl.textContent = formatMethod(method, states); copyBtn.hidden = !states.every((state) => state.valid); } if (parameters.length === 0) { const empty = el("span", "cm-empty"); empty.textContent = "该方法没有参数"; rowEl.appendChild(empty); updatePreview(); return; } for (const parameter of parameters) { const field = el("label", "cm-param"); field.title = parameter.description; const label = el("span", "cm-param-name"); label.textContent = parameter.name; const control = makeControl(parameter, updatePreview); field.appendChild(label); field.appendChild(control); rowEl.appendChild(field); controls.push({ parameter, control }); } updatePreview(); } function updateSuggestions() { clearSuggest(); const matches = searchMethods(queryEl.value); for (const method of matches) { const item = el("li", "cm-suggest-item"); const nameSpan = el("span", "cm-suggest-name"); nameSpan.textContent = method.name; const descSpan = el("span", "cm-suggest-desc"); descSpan.textContent = method.description; item.appendChild(nameSpan); item.appendChild(descSpan); item.addEventListener("mousedown", (event) => { event.preventDefault(); selectMethod(method); }); suggestEl.appendChild(item); } } queryEl.addEventListener("input", () => { updateSuggestions(); selectedEl.hidden = true; }); queryEl.addEventListener("focus", updateSuggestions); queryEl.addEventListener("blur", () => { window.setTimeout(clearSuggest, 120); }); if (queryEl.value) { updateSuggestions(); } } function initAll() { const containers = document.querySelectorAll(".custommethods-search"); containers.forEach((container) => renderContainer(container)); } function register() { const docsify = window.$docsify; if (docsify) { docsify.plugins = [].concat(docsify.plugins, (hook) => { hook.doneEach(() => initAll()); }); } else if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => initAll()); } else { initAll(); } } window.CustomMethodSearch = { search: searchMethods, editDistance, renderAll: initAll, getMethods: () => methods, setMethods: (list) => { methods = list; initAll(); }, Parameter, ParameterType, Method, MethodType, Mod, Surgery, CommentMethod, InkMethod, CustomMethods, VariableMethod, ConditionMethod, CustomClassMethod, }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", register); } else { register(); } })();