feat(js): 千星奇域每周经验刷取(回放通关版) (#2425)
50
repo/js/MiliastraExperiencePlayback/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 🌌 千星奇域·每周经验刷取(回放通关版)
|
||||
|
||||
### 🌟 功能介绍
|
||||
|
||||
- 🔁 自动重复通关指定的奇域关卡。
|
||||
- 🎥 依据用户预先录制的多个通关流程,随机选取一个执行通关。
|
||||
- 🧺 支持删除关卡存档后重新挑战(默认关闭),从而提升刷取效率。
|
||||
- 📅 自动追踪每周经验获取进度,达到上限后再次执行将自动跳过。
|
||||
- 🏞️ 执行结束后自动返回提瓦特大陆,避免影响其他自动化流程。
|
||||
|
||||
## 📋 使用必读
|
||||
|
||||
- 请勿在禁止联机的区域(如银月之庭等)运行本脚本,否则将无法运行。
|
||||
- 如果开启“删除关卡存档”功能, **建议提前清理无用存档** ,保持存档列表简洁,以提升查找效率、减少误差。
|
||||
- 若内置的通关流程失效,请使用程序的 **[录制回放]** 功能,自行录制 **进入关卡后的通关流程** 。
|
||||
- 录制完成后,打开录制回放所在目录,将录制的脚本拷贝到本脚本目录下的 `assets/playbacks` 文件夹中。
|
||||
- 右键编辑脚本配置,根据你所录制的流程调整相关设置(包括奇域关卡、通关回放文件池、每次通关获取的经验值等)。
|
||||
- 建议选择人气较高、不易下架、流程简单的单人游玩关卡进行录制(例如石头模拟器、风景打卡类、抽卡模拟器等)。
|
||||
- 根据成就完成数量的不同,每次通关可额外获得 **50~250** 点经验值。内置的通关流程设计相对保守,每次通关可获得 20 点基础经验 + 50 点成就经验。
|
||||
- ~~人有多大胆,地有多大产~~。**建议适当延长录制时的通关时间**,以降低关卡下架风险和自身运行风险。
|
||||
|
||||
### 📖 使用方法
|
||||
|
||||
- 将本脚本添加至您的配置组中。
|
||||
- 右键点击脚本自定义配置项。
|
||||
- 配置好计划通关的关卡信息、通关回放文件等。
|
||||
- 点击运行按钮。
|
||||
|
||||
### 🛠️ 脚本配置
|
||||
|
||||
| 配置项 | 描述 | 备注 | 默认值 |
|
||||
| ---------------------- | ------------------------ | ---------------------------------------- | ----------------------------- |
|
||||
| room | 奇域关卡关键词或关卡GUID | | 20134075027 |
|
||||
| playbacks | 通关回放文件池 | 自行录制拷贝到assets/playbacks,逗号分隔 | 通关回放1.json,通关回放2.json |
|
||||
| expPerAttempt | 每次通关获取的经验值 | 如果勾选删除关卡存档,请自行增加 | 20 |
|
||||
| deleteStageSave | 删除关卡存档 | 可重复达成成就,获取更多经验值 | false |
|
||||
| deleteStageSaveKeyword | 删除关卡存档关键字 | 关卡存档视图中的[关卡]列 | 深渊100层 |
|
||||
| expWeeklyLimit | 每周可获取的经验值上限 | | 4000 |
|
||||
| force | 忽略本周经验值已达上限 | | false |
|
||||
| thisAttempts | 指定通关次数 | 0表示自动判断 | 0 |
|
||||
| goToTeyvat | 完成后返回提瓦特大陆 | | true |
|
||||
|
||||
### ❗ 注意事项
|
||||
|
||||
- 请确保游戏窗口分辨率比例为 `16:9`,否则可能影响脚本正常运行。
|
||||
- 建议提前设置好快捷键,便于在需要时快速暂停或终止脚本。
|
||||
|
||||
## 🐛 问题反馈
|
||||
|
||||
[breadgrocery](https://github.com/breadgrocery/miliastra-experience-playback)
|
||||
BIN
repo/js/MiliastraExperiencePlayback/assets/Checkbox_Checked.png
Normal file
|
After Width: | Height: | Size: 143 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
repo/js/MiliastraExperiencePlayback/assets/UI_BtnIcon_Close.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
repo/js/MiliastraExperiencePlayback/assets/UI_BtnIcon_Gacha.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
repo/js/MiliastraExperiencePlayback/assets/UI_Icon_Leave.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"macroEvents": [
|
||||
{ "type": 0, "keyCode": 65, "mouseX": 0, "mouseY": 0, "time": 547 },
|
||||
{ "type": 0, "keyCode": 32, "mouseX": 0, "mouseY": 0, "time": 1109 },
|
||||
{ "type": 1, "keyCode": 32, "mouseX": 0, "mouseY": 0, "time": 1218 },
|
||||
{ "type": 1, "keyCode": 65, "mouseX": 0, "mouseY": 0, "time": 8015 },
|
||||
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9188 },
|
||||
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9344 },
|
||||
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9531 },
|
||||
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9703 },
|
||||
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9906 },
|
||||
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 10078 }
|
||||
],
|
||||
"info": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"x": 319,
|
||||
"y": 278,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"recordDpi": 1.5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"macroEvents": [
|
||||
{ "type": 0, "keyCode": 68, "mouseX": 0, "mouseY": 0, "time": 547 },
|
||||
{ "type": 0, "keyCode": 32, "mouseX": 0, "mouseY": 0, "time": 1109 },
|
||||
{ "type": 1, "keyCode": 32, "mouseX": 0, "mouseY": 0, "time": 1218 },
|
||||
{ "type": 1, "keyCode": 68, "mouseX": 0, "mouseY": 0, "time": 8015 },
|
||||
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9188 },
|
||||
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9344 },
|
||||
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9531 },
|
||||
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9703 },
|
||||
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 9906 },
|
||||
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 10078 }
|
||||
],
|
||||
"info": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"x": 319,
|
||||
"y": 278,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"recordDpi": 1.5
|
||||
}
|
||||
}
|
||||
905
repo/js/MiliastraExperiencePlayback/main.js
Normal file
@@ -0,0 +1,905 @@
|
||||
/**
|
||||
* Better Genshin Impact JavaScript
|
||||
* Bundled with BetterGI CLI (https://www.npmjs.com/package/@bettergi/cli)
|
||||
*
|
||||
* This file is automatically generated and should not be edited.
|
||||
*/
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/workflow.js
|
||||
var defaultMaxAttempts = 5;
|
||||
var defaultRetryInterval = 1e3;
|
||||
var waitForAction = async (condition, retryAction, options) => {
|
||||
const { maxAttempts = defaultMaxAttempts, retryInterval = defaultRetryInterval } = options || {};
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
if (i === 0 && condition())
|
||||
return true;
|
||||
await retryAction?.();
|
||||
await sleep(retryInterval);
|
||||
if (condition())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
var waitForRegionAppear = async (regionProvider, retryAction, options) => {
|
||||
return waitForAction(() => {
|
||||
const region = regionProvider();
|
||||
return region != null && region.isExist();
|
||||
}, retryAction, options);
|
||||
};
|
||||
var waitForRegionDisappear = async (regionProvider, retryAction, options) => {
|
||||
return waitForAction(() => {
|
||||
const region = regionProvider();
|
||||
return !region || !region.isExist();
|
||||
}, retryAction, options);
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/asserts.js
|
||||
var assertRegionAppearing = async (regionProvider, message, retryAction, options) => {
|
||||
const isAppeared = await waitForRegionAppear(regionProvider, retryAction, options);
|
||||
if (!isAppeared) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
var assertRegionDisappearing = async (regionProvider, message, retryAction, options) => {
|
||||
const isDisappeared = await waitForRegionDisappear(regionProvider, retryAction, options);
|
||||
if (!isDisappeared) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/exception.js
|
||||
var getErrorMessage = (err) => {
|
||||
if (err && "message" in err && typeof err.message === "string")
|
||||
return err.message;
|
||||
return err && typeof err === "object" ? JSON.stringify(err) : "Unknown error";
|
||||
};
|
||||
var isHostException = (err) => {
|
||||
return err && "hostException" in err;
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/mouse.js
|
||||
var simulateScroll = async (scrollAmountInClicks, times) => {
|
||||
const script = {
|
||||
macroEvents: Array(times).fill({ type: 6, mouseX: 0, mouseY: scrollAmountInClicks, time: 0 }),
|
||||
info: { name: "", description: "", x: 0, y: 0, width: 1920, height: 1080, recordDpi: 1.5 }
|
||||
};
|
||||
await keyMouseScript.run(JSON.stringify(script));
|
||||
};
|
||||
var mouseScrollDown = (height, algorithm = (h) => Math.floor(h / 18)) => {
|
||||
return simulateScroll(-120, algorithm(height));
|
||||
};
|
||||
var mouseScrollDownLines = (lines, lineHeight = 175) => {
|
||||
return mouseScrollDown(lines * lineHeight);
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/ocr.js
|
||||
var findImageWithinBounds = (image, x, y, w, h, config = {}) => {
|
||||
const ir = captureGameRegion();
|
||||
try {
|
||||
const mat = typeof image === "string" ? file.readImageMatSync(image) : image;
|
||||
const ro = RecognitionObject.templateMatch(mat, x, y, w, h);
|
||||
if (Object.keys(config).length > 0) {
|
||||
Object.assign(ro, config) && ro.initTemplate();
|
||||
}
|
||||
const region = ir.find(ro);
|
||||
return region.isExist() ? region : void 0;
|
||||
} catch (err) {
|
||||
log.warn(`${err.message || err}`);
|
||||
} finally {
|
||||
ir.dispose();
|
||||
}
|
||||
};
|
||||
var findFirst = (ir, ro, predicate) => {
|
||||
const candidates = ir.findMulti(ro);
|
||||
for (let i = 0; i < candidates.count; i++) {
|
||||
if (predicate(candidates[i]))
|
||||
return candidates[i];
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
var findTextWithinBounds = (text, x, y, w, h, options, config = {}) => {
|
||||
const { ignoreCase = true, contains = false } = options || {};
|
||||
const searchText = ignoreCase ? text.toLowerCase() : text;
|
||||
const ir = captureGameRegion();
|
||||
try {
|
||||
const ro = RecognitionObject.ocr(x, y, w, h);
|
||||
if (Object.keys(config).length > 0) {
|
||||
Object.assign(ro, config) && ro.initTemplate();
|
||||
}
|
||||
return findFirst(ir, ro, (region) => {
|
||||
const itemText = ignoreCase ? region.text.toLowerCase() : region.text;
|
||||
const isMatch = contains ? itemText.includes(searchText) : itemText === searchText;
|
||||
return isMatch && region.isExist();
|
||||
});
|
||||
} catch (err) {
|
||||
log.warn(`${err.message || err}`);
|
||||
} finally {
|
||||
ir.dispose();
|
||||
}
|
||||
};
|
||||
var findTextWithinListView = async (text, listView, matchOptions, retryOptions, config = {}) => {
|
||||
const { x, y, w, h, lineHeight, scrollLines = 1, paddingX = 10, paddingY = 10 } = listView;
|
||||
const { maxAttempts = 30, retryInterval = 1e3 } = retryOptions || {};
|
||||
const findTargetText = () => findTextWithinBounds(text, x, y, w, h, matchOptions, config);
|
||||
let lastTextRegion;
|
||||
const isReachedBottom = () => {
|
||||
const textRegion = findFirst(captureGameRegion(), RecognitionObject.ocr(x, y, w, h), (region) => {
|
||||
return region.isExist() && region.text.trim().length > 0;
|
||||
});
|
||||
if (textRegion) {
|
||||
if (lastTextRegion?.text === textRegion.text && Math.abs(textRegion.y - lastTextRegion.y) < lineHeight) {
|
||||
return true;
|
||||
} else {
|
||||
lastTextRegion = textRegion;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const isTextFoundOrBottomReached = await waitForAction(() => findTargetText() != void 0 || isReachedBottom(), async () => {
|
||||
moveMouseTo(x + w - paddingX, y + paddingY);
|
||||
await sleep(50);
|
||||
await mouseScrollDownLines(scrollLines, lineHeight);
|
||||
}, { maxAttempts, retryInterval });
|
||||
return isTextFoundOrBottomReached ? findTargetText() : void 0;
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/misc.js
|
||||
var deepMerge = (...objects) => {
|
||||
const isPlainObject = (input) => input?.constructor === Object;
|
||||
return objects.reduce((result, obj) => {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
const recursive = isPlainObject(acc[key]) && isPlainObject(value);
|
||||
acc[key] = recursive ? deepMerge(acc[key], value) : value;
|
||||
return acc;
|
||||
}, result);
|
||||
}, {});
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/time.js
|
||||
var getNextMonday4AM = () => {
|
||||
const now = /* @__PURE__ */ new Date();
|
||||
const result = new Date(now);
|
||||
result.setHours(4, 0, 0, 0);
|
||||
const currentDay = now.getDay();
|
||||
const daysUntilNextMonday = currentDay === 1 && now.getHours() < 4 ? 0 : 8 - currentDay;
|
||||
result.setDate(now.getDate() + daysUntilNextMonday);
|
||||
return result;
|
||||
};
|
||||
var parseDuration = (duration) => {
|
||||
return {
|
||||
h: Math.floor(duration / 36e5),
|
||||
m: Math.floor(duration % 36e5 / 6e4),
|
||||
s: Math.floor(duration % 6e4 / 1e3),
|
||||
ms: Math.floor(duration % 1e3)
|
||||
};
|
||||
};
|
||||
var formatDurationAsClock = (duration) => {
|
||||
return Object.values(parseDuration(duration)).slice(0, 3).map((num) => String(num).padStart(2, "0")).join(":");
|
||||
};
|
||||
var formatDurationAsReadable = (duration) => {
|
||||
return Object.entries(parseDuration(duration)).filter(([, value]) => value > 0).map(([unit, value]) => `${value}${unit}`).join(" ");
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/progress.js
|
||||
var ProgressTracker = class {
|
||||
total = 0;
|
||||
current = 0;
|
||||
startTime = Date.now();
|
||||
formatter;
|
||||
interval;
|
||||
lastPrintTime = 0;
|
||||
constructor(total, config) {
|
||||
const { formatter, interval = 3e3 } = config || {};
|
||||
this.total = total;
|
||||
this.formatter = formatter || this.defaultFormatter;
|
||||
this.interval = interval;
|
||||
}
|
||||
defaultFormatter = (logger, message, progress) => {
|
||||
logger("[🚧 {pct} ⏳ {eta}]: {msg}", progress.formatted.percentage.padStart(6), progress.current > 0 && progress.elapsed > 0 ? progress.formatted.remaining : "--:--:--", message);
|
||||
};
|
||||
tick(options) {
|
||||
const { increment = 1, message, force = false } = options || {};
|
||||
this.current = Math.min(this.current + increment, this.total);
|
||||
if (message)
|
||||
this.print(message, force);
|
||||
return this.current === this.total;
|
||||
}
|
||||
complete(message) {
|
||||
this.current = this.total;
|
||||
this.print(message, true);
|
||||
}
|
||||
reset() {
|
||||
this.current = 0;
|
||||
this.startTime = Date.now();
|
||||
this.lastPrintTime = 0;
|
||||
}
|
||||
print(message, force = false, logger = log.info) {
|
||||
if (force || this.shouldPrint()) {
|
||||
this.formatter(logger, message, this.getProgress());
|
||||
this.printed();
|
||||
}
|
||||
}
|
||||
shouldPrint() {
|
||||
return Date.now() - this.lastPrintTime >= this.interval;
|
||||
}
|
||||
printed() {
|
||||
this.lastPrintTime = Date.now();
|
||||
}
|
||||
getProgress() {
|
||||
const percentage = this.current / this.total;
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
const average = this.current > 0 ? elapsed / this.current : 0;
|
||||
const remaining = (this.total - this.current) * average;
|
||||
return {
|
||||
current: this.current,
|
||||
total: this.total,
|
||||
percentage,
|
||||
elapsed,
|
||||
average,
|
||||
remaining,
|
||||
formatted: {
|
||||
percentage: `${(percentage * 100).toFixed(1)}%`,
|
||||
elapsed: formatDurationAsReadable(elapsed),
|
||||
average: formatDurationAsReadable(average),
|
||||
remaining: formatDurationAsClock(remaining)
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/store.js
|
||||
var useStore = (name) => {
|
||||
const filePath = `store/${name}.json`;
|
||||
const obj = (() => {
|
||||
try {
|
||||
const storeFiles = [...file.readPathSync("store")].map((path) => path.replace(/\\/g, "/"));
|
||||
if (!storeFiles.includes(filePath))
|
||||
throw new Error("File does not exist");
|
||||
const text = file.readTextSync(filePath);
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
const createProxy = (target, parentPath = []) => {
|
||||
if (typeof target !== "object" || target === null) {
|
||||
return target;
|
||||
}
|
||||
return new Proxy(target, {
|
||||
get: (target2, key) => {
|
||||
const value = Reflect.get(target2, key);
|
||||
return typeof value === "object" && value !== null ? createProxy(value, [...parentPath, key]) : value;
|
||||
},
|
||||
set: (target2, key, value) => {
|
||||
const success = Reflect.set(target2, key, value);
|
||||
if (success) {
|
||||
Promise.resolve().then(() => {
|
||||
file.writeTextSync(filePath, JSON.stringify(obj, null, 2));
|
||||
});
|
||||
}
|
||||
return success;
|
||||
},
|
||||
deleteProperty: (target2, key) => {
|
||||
const success = Reflect.deleteProperty(target2, key);
|
||||
if (success) {
|
||||
Promise.resolve().then(() => {
|
||||
file.writeTextSync(filePath, JSON.stringify(obj, null, 2));
|
||||
});
|
||||
}
|
||||
return success;
|
||||
}
|
||||
});
|
||||
};
|
||||
return createProxy(obj);
|
||||
};
|
||||
var useStoreWithDefaults = (name, defaults) => {
|
||||
const store = useStore(name);
|
||||
Object.assign(store, deepMerge(defaults, store));
|
||||
return store;
|
||||
};
|
||||
|
||||
// src/config.ts
|
||||
var userConfig = {
|
||||
room: settings.room || "20134075027",
|
||||
playbacks: (settings.playbacks || "通关回放1.json,通关回放2.json").replace(/,/g, ",").split(",").map((str) => str.trim()).filter(Boolean),
|
||||
expPerAttempt: Math.max(1, Number(settings.expPerAttempt || "20")),
|
||||
deleteStageSave: settings.deleteStageSave ?? false,
|
||||
deleteStageSaveKeyword: settings.deleteStageSaveKeyword || "深渊100层",
|
||||
expWeeklyLimit: Math.max(1, Number(settings.expWeeklyLimit || "4000")),
|
||||
force: settings.force ?? false,
|
||||
thisAttempts: Math.max(0, Number(settings.thisAttempts || "0")),
|
||||
goToTeyvat: settings.goToTeyvat ?? true
|
||||
};
|
||||
|
||||
// src/regions.ts
|
||||
//! 查找确认按钮
|
||||
var findConfirmBtn = () => {
|
||||
return findTextWithinBounds("确认", 480, 720, 960, 145);
|
||||
};
|
||||
//! 查找标题文字
|
||||
var findHeaderTitle = (title, contains) => {
|
||||
return findTextWithinBounds(title, 0, 0, 300, 95, { contains });
|
||||
};
|
||||
//! 查找底部按钮文字
|
||||
var findBottomBtnText = (text, contains) => {
|
||||
return findTextWithinBounds(text, 960, 980, 960, 100, { contains });
|
||||
};
|
||||
//! 查找关闭对话框按钮
|
||||
var findCloseDialog = () => {
|
||||
const img = "assets/UI_BtnIcon_Close.png";
|
||||
const iro = findImageWithinBounds(img, 480, 216, 960, 648, {
|
||||
threshold: 0.85
|
||||
});
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 查找抽卡按钮(判断处于大世界条件一)
|
||||
var findGachaBtn = () => {
|
||||
const img = "assets/UI_BtnIcon_Gacha.png";
|
||||
const iro = findImageWithinBounds(img, 960, 0, 960, 80, { threshold: 0.85 });
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 查找推荐奇域按钮(判断处于大世界条件二)
|
||||
var findBeyondRecommendBtn = () => {
|
||||
const img = "assets/UI_BtnIcon_Beyond_Recommend.png";
|
||||
const iro = findImageWithinBounds(img, 960, 0, 960, 80, { threshold: 0.85 });
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 查找奇域大厅按钮(判断处于奇域大厅)
|
||||
var findBeyondHallBtn = () => {
|
||||
const img = "assets/UI_BtnIcon_Beyond_Hall.png";
|
||||
const iro = findImageWithinBounds(img, 200, 0, 150, 100, { threshold: 0.85 });
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 全部奇域按钮
|
||||
var findAllWonderlandsBtn = () => {
|
||||
return findTextWithinBounds("全部", 1320, 0, 600, 95, { contains: true });
|
||||
};
|
||||
//! 查找奇域搜索输入框
|
||||
var findSearchWonderlandInput = () => {
|
||||
return findTextWithinBounds("搜索", 0, 120, 1920, 60, { contains: true });
|
||||
};
|
||||
//! 查找奇域搜索输入框清除按钮
|
||||
var findClearInputBtn = () => {
|
||||
return findTextWithinBounds("清除", 0, 120, 1920, 60);
|
||||
};
|
||||
//! 查找搜索奇域按钮
|
||||
var findSearchWonderlandBtn = () => {
|
||||
return findTextWithinBounds("搜索", 0, 120, 1920, 60, { contains: true });
|
||||
};
|
||||
//! 查找搜索过于频繁提示
|
||||
var findSearchWonderlandThrottleMsg = () => {
|
||||
return findTextWithinBounds("过于频繁", 0, 0, 1920, 300, { contains: true });
|
||||
};
|
||||
//! 查找第一个奇域搜索结果名称
|
||||
var findFirstSearchResultText = () => {
|
||||
const ir = captureGameRegion();
|
||||
const ro = RecognitionObject.ocr(240, 390, 300, 50);
|
||||
return (() => {
|
||||
const list = ir.findMulti(ro);
|
||||
for (let i = 0; i < list.count; i++) {
|
||||
if (list[i] && list[i].isExist()) {
|
||||
return list[i].text;
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
//! 点击选择第一个搜索结果
|
||||
var clickToChooseFirstSearchResult = () => {
|
||||
click(355, 365);
|
||||
};
|
||||
//! 查找进入房间快捷键按钮
|
||||
var findEnterRoomShortcut = () => {
|
||||
return findTextWithinBounds("房间", 1580, 110, 320, 390, { contains: true });
|
||||
};
|
||||
//! 查找退出房间按钮
|
||||
var findLeaveRoomBtn = () => {
|
||||
const img = "assets/UI_Icon_Leave_Right.png";
|
||||
const iro = findImageWithinBounds(img, 1570, 0, 350, 100);
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 查找跳转大厅按钮
|
||||
var findGoToLobbyBtn = () => {
|
||||
return findTextWithinBounds("大厅", 880, 840, 1040, 110, {
|
||||
contains: true
|
||||
});
|
||||
};
|
||||
//! 查找创建房间按钮
|
||||
var findCreateRoomBtn = () => {
|
||||
return findTextWithinBounds("房间", 960, 140, 960, 70, { contains: true });
|
||||
};
|
||||
//! 点击加入准备区
|
||||
var clickToPrepare = () => {
|
||||
click(770, 275);
|
||||
};
|
||||
//! 加入准备区提示
|
||||
var findPrepareMsg = () => {
|
||||
return findTextWithinBounds("加入准备", 576, 432, 768, 216, {
|
||||
contains: true
|
||||
});
|
||||
};
|
||||
//! 查找奇域收藏
|
||||
var findBeyondFavoritesBtn = () => {
|
||||
return findTextWithinBounds("收藏", 0, 880, 200, 200, {
|
||||
contains: true
|
||||
});
|
||||
};
|
||||
//! 查找管理关卡按钮
|
||||
var findManageStagesBtn = () => {
|
||||
return findTextWithinBounds("管理", 1320, 0, 600, 95, { contains: true });
|
||||
};
|
||||
//! 查找编辑关卡存档按钮
|
||||
var findEditStageSaveBtn = () => {
|
||||
return findTextWithinBounds("管理", 1220, 980, 700, 100);
|
||||
};
|
||||
//! 查找要删除的存档位置
|
||||
var findSaveToDeletePos = (keyword) => findTextWithinListView(
|
||||
keyword,
|
||||
{
|
||||
x: 210,
|
||||
y: 250,
|
||||
w: 1650,
|
||||
h: 710,
|
||||
scrollLines: 7,
|
||||
lineHeight: 95
|
||||
},
|
||||
{ contains: true }
|
||||
);
|
||||
//! 查找局外存档列头
|
||||
var findExternalSaveColumnPos = () => {
|
||||
return findTextWithinBounds("局外", 55, 190, 1810, 50, { contains: true });
|
||||
};
|
||||
//! 查找删除局外存档复选框已选中状态
|
||||
var findDeleteExternalSaveChecked = (colPos) => {
|
||||
const img = "assets/Checkbox_Checked.png";
|
||||
const iro = findImageWithinBounds(img, colPos, 250, 290, 710, {
|
||||
threshold: 0.6,
|
||||
use3Channels: false
|
||||
});
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 查找删除关卡存档按钮
|
||||
var findDeleteStageSaveBtn = () => {
|
||||
return findTextWithinBounds("删除所选", 1220, 980, 700, 100);
|
||||
};
|
||||
//! 查找关卡退出按钮
|
||||
var findStageEscBtn = () => {
|
||||
const img = "assets/UI_Icon_Leave.png";
|
||||
const iro = findImageWithinBounds(img, 0, 0, 100, 100, { threshold: 0.75 });
|
||||
iro?.drawSelf("group_img");
|
||||
return iro;
|
||||
};
|
||||
//! 查找中断挑战按钮
|
||||
var findExitStageBtn = () => {
|
||||
return findTextWithinBounds("中断挑战", 576, 324, 768, 432);
|
||||
};
|
||||
//! 查找跳过奇域等级提升页面
|
||||
var findSkipLevelUpMsg = () => {
|
||||
return findTextWithinBounds("空白处", 610, 950, 700, 60, { contains: true });
|
||||
};
|
||||
//! 查找返回提瓦特按钮
|
||||
var findGotTeyvatBtn = () => {
|
||||
return findTextWithinBounds("返回", 1500, 0, 300, 95, { contains: true });
|
||||
};
|
||||
|
||||
// src/lobby.ts
|
||||
//! 判断是否处于奇域大厅
|
||||
var isInLobby = () => findBeyondHallBtn() !== void 0;
|
||||
//! 判断是否处于提瓦特大陆
|
||||
var isInTeyvat = () => {
|
||||
return findGachaBtn() !== void 0 && findBeyondRecommendBtn() !== void 0;
|
||||
};
|
||||
//! 退出大厅返回提瓦特大陆
|
||||
var exitLobbyToTeyvat = async () => {
|
||||
if (!userConfig.goToTeyvat) return;
|
||||
if (isInTeyvat()) {
|
||||
log.warn("已处于提瓦特大陆,跳过");
|
||||
return;
|
||||
}
|
||||
log.info("打开当前大厅...");
|
||||
await assertRegionAppearing(
|
||||
() => findHeaderTitle("大厅", true),
|
||||
"打开当前大厅超时",
|
||||
() => {
|
||||
keyPress("VK_F2");
|
||||
},
|
||||
{ maxAttempts: 10, retryInterval: 2e3 }
|
||||
);
|
||||
log.info("返回提瓦特大陆...");
|
||||
const done = await waitForAction(
|
||||
isInTeyvat,
|
||||
() => {
|
||||
findGotTeyvatBtn()?.click();
|
||||
findConfirmBtn()?.click();
|
||||
},
|
||||
{ maxAttempts: 120 }
|
||||
);
|
||||
if (!done) throw new Error("返回提瓦特大陆超时");
|
||||
};
|
||||
|
||||
// src/room.ts
|
||||
var isInRoom = () => findHeaderTitle("房间", true) !== void 0;
|
||||
//! 打开人气奇域
|
||||
var goToRecommendedWonderlands = async () => {
|
||||
log.info("打开人气奇域界面...");
|
||||
await assertRegionAppearing(
|
||||
() => findHeaderTitle("人气", true),
|
||||
"打开人气奇域界面超时",
|
||||
() => {
|
||||
keyPress("VK_F6");
|
||||
}
|
||||
);
|
||||
};
|
||||
//! 创建并进入奇域房间
|
||||
var createRoom = async (room) => {
|
||||
await goToRecommendedWonderlands();
|
||||
log.info("打开全部奇域界面...");
|
||||
await assertRegionAppearing(
|
||||
() => findHeaderTitle("全部", true),
|
||||
"打开全部奇域界面超时",
|
||||
() => {
|
||||
findAllWonderlandsBtn()?.click();
|
||||
}
|
||||
);
|
||||
await sleep(1500);
|
||||
//! 记录搜索前的第一个奇域名称
|
||||
let iwnt;
|
||||
let wi = 0;
|
||||
while (iwnt === void 0) {
|
||||
if (wi > 20) break;
|
||||
iwnt = findFirstSearchResultText();
|
||||
await sleep(500);
|
||||
wi += 1;
|
||||
}
|
||||
if (iwnt === void 0) throw new Error("加载全部奇域列表超时");
|
||||
log.info("搜索前的第一个奇域名称: {iwnt}", iwnt);
|
||||
log.info("粘贴奇域关卡文本: {room}", room);
|
||||
await assertRegionAppearing(findClearInputBtn, "粘贴关卡文本超时", () => {
|
||||
const input = findSearchWonderlandInput();
|
||||
if (input) {
|
||||
input.click();
|
||||
inputText(room);
|
||||
}
|
||||
});
|
||||
//! 等待搜索结果变化
|
||||
let fswnt;
|
||||
log.info("搜索奇域关卡: {room}", room);
|
||||
await waitForAction(
|
||||
() => {
|
||||
if (fswnt === void 0) return false;
|
||||
//! 检测搜索过于频繁提示
|
||||
if (findSearchWonderlandThrottleMsg()) return true;
|
||||
//! 检测搜索结果是否变化
|
||||
return fswnt.toLocaleLowerCase().trim() !== iwnt.toLocaleLowerCase().trim();
|
||||
},
|
||||
async () => {
|
||||
findSearchWonderlandBtn()?.click();
|
||||
await sleep(1e3);
|
||||
fswnt = findFirstSearchResultText();
|
||||
},
|
||||
{ maxAttempts: 30, retryInterval: 200 }
|
||||
);
|
||||
log.info("打开奇域介绍...");
|
||||
await assertRegionAppearing(
|
||||
findCreateRoomBtn,
|
||||
"打开奇域介绍超时",
|
||||
() => {
|
||||
const goToLobbyButton = findGoToLobbyBtn();
|
||||
if (goToLobbyButton) {
|
||||
log.info("当前不在大厅,前往大厅...");
|
||||
goToLobbyButton.click();
|
||||
} else {
|
||||
log.info("选择第一个奇域关卡...");
|
||||
clickToChooseFirstSearchResult();
|
||||
}
|
||||
},
|
||||
{ maxAttempts: 60 }
|
||||
);
|
||||
log.info("创建并进入房间...");
|
||||
await assertRegionAppearing(
|
||||
() => findHeaderTitle("房间", true),
|
||||
"创建并进入房间超时",
|
||||
() => {
|
||||
findCreateRoomBtn()?.click();
|
||||
},
|
||||
{ maxAttempts: 60 }
|
||||
);
|
||||
};
|
||||
//! 进入奇域房间
|
||||
var enterRoom = async (room) => {
|
||||
const inLobby = isInLobby();
|
||||
if (inLobby) {
|
||||
const enterButton = findEnterRoomShortcut();
|
||||
if (enterButton) {
|
||||
log.info("当前已存在房间,进入房间...", room);
|
||||
await assertRegionAppearing(
|
||||
() => findHeaderTitle("房间", true),
|
||||
"进入房间超时",
|
||||
() => {
|
||||
keyPress("VK_P");
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.info("当前不在房间内,创建房间...", room);
|
||||
await createRoom(room);
|
||||
};
|
||||
//! 离开房间
|
||||
var leaveRoom = async () => {
|
||||
//! 当前在大厅,且存在房间
|
||||
if (isInLobby() && findEnterRoomShortcut() !== void 0 || isInRoom()) {
|
||||
log.info("当前存在房间,离开房间...");
|
||||
//! 先进入房间
|
||||
await assertRegionAppearing(
|
||||
() => findHeaderTitle("房间", true),
|
||||
"进入房间超时",
|
||||
() => {
|
||||
keyPress("VK_P");
|
||||
}
|
||||
);
|
||||
//! 离开房间
|
||||
await assertRegionAppearing(
|
||||
findBeyondHallBtn,
|
||||
"离开房间超时",
|
||||
async () => {
|
||||
findLeaveRoomBtn()?.click();
|
||||
await sleep(1e3);
|
||||
findConfirmBtn()?.click();
|
||||
},
|
||||
{ maxAttempts: 5 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// src/save.ts
|
||||
//! 进入管理关卡存档界面
|
||||
var goToManageStageSave = async () => {
|
||||
//! 打开人气奇域
|
||||
await goToRecommendedWonderlands();
|
||||
//! 打开奇域收藏->管理关卡
|
||||
await assertRegionAppearing(
|
||||
findEditStageSaveBtn,
|
||||
"打开编辑关卡存档按钮超时",
|
||||
async () => {
|
||||
//! 点击奇域收藏
|
||||
findBeyondFavoritesBtn()?.click();
|
||||
await sleep(300);
|
||||
//! 点击管理关卡
|
||||
findManageStagesBtn()?.click();
|
||||
await sleep(300);
|
||||
},
|
||||
{ maxAttempts: 5 }
|
||||
);
|
||||
};
|
||||
//! 删除关卡存档
|
||||
var deleteStageSave = async () => {
|
||||
if (!userConfig.deleteStageSave || userConfig.deleteStageSaveKeyword.trim() === "") {
|
||||
log.info("未启用删除关卡存档,跳过");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
//! 进入管理关卡存档界面
|
||||
await goToManageStageSave();
|
||||
//! 选中要删除的关卡的局外存档
|
||||
const stagePos = await findSaveToDeletePos(userConfig.deleteStageSaveKeyword);
|
||||
if (stagePos === void 0) {
|
||||
log.warn("未找到要删除的关卡存档,跳过");
|
||||
return;
|
||||
}
|
||||
const colPos = findExternalSaveColumnPos();
|
||||
if (colPos === void 0) {
|
||||
log.warn("无法确定关卡的局外存档列位置,跳过");
|
||||
return;
|
||||
}
|
||||
//! 进入编辑模式
|
||||
await assertRegionDisappearing(
|
||||
findEditStageSaveBtn,
|
||||
"进入编辑模式超时",
|
||||
() => {
|
||||
keyPress("VK_F");
|
||||
},
|
||||
{ maxAttempts: 5 }
|
||||
);
|
||||
//! 计算勾选框位置并点击
|
||||
const [cx, cy] = [(colPos.x * 2 + colPos.width) / 2, stagePos.y + 20];
|
||||
await assertRegionAppearing(
|
||||
() => findDeleteExternalSaveChecked(colPos.x),
|
||||
"勾选要删除的局外存档超时",
|
||||
() => {
|
||||
click(Math.ceil(cx), Math.ceil(cy));
|
||||
},
|
||||
{ maxAttempts: 5, retryInterval: 1500 }
|
||||
);
|
||||
//! 点击删除所选按钮
|
||||
await assertRegionDisappearing(
|
||||
() => findDeleteExternalSaveChecked(colPos.x),
|
||||
"删除关卡存档超时",
|
||||
async () => {
|
||||
//! 特征较为脆弱,多次确认,确保成功删除
|
||||
findConfirmBtn()?.click();
|
||||
await sleep(500);
|
||||
findConfirmBtn()?.click();
|
||||
findDeleteStageSaveBtn()?.click();
|
||||
await sleep(1e3);
|
||||
findConfirmBtn()?.click();
|
||||
await sleep(500);
|
||||
findConfirmBtn()?.click();
|
||||
},
|
||||
{
|
||||
maxAttempts: 5
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
if (isHostException(err)) throw err;
|
||||
log.warn("删除关卡存档失败: {error}", getErrorMessage(err));
|
||||
} finally {
|
||||
//! 返回大厅
|
||||
await genshin.returnMainUi();
|
||||
}
|
||||
};
|
||||
|
||||
// src/stage.ts
|
||||
//! 可用的执行通关回放文件列表
|
||||
var availablePlaybackFiles = () => {
|
||||
const files = [...file.readPathSync("assets/playbacks")].map((path) => path.replace(/\\/g, "/"));
|
||||
return userConfig.playbacks.map((file2) => `assets/playbacks/${file2}`).filter((path) => files.includes(path));
|
||||
};
|
||||
//! 确保通关回放文件存在
|
||||
var ensurePlaybackFilesExist = () => {
|
||||
const list = availablePlaybackFiles();
|
||||
if (list.length === 0) {
|
||||
throw new Error("未找到任何通关回放文件,请确保已录制回放并拷贝到 assets/playbacks 目录下");
|
||||
}
|
||||
};
|
||||
var playStage = async () => {
|
||||
//! 等待进入关卡
|
||||
await assertRegionAppearing(
|
||||
findStageEscBtn,
|
||||
"等待进入关卡超时",
|
||||
async () => {
|
||||
findBottomBtnText("开始游戏")?.click();
|
||||
findBottomBtnText("准备", true)?.click();
|
||||
//! 判断是否已经加入准备区
|
||||
if (findPrepareMsg()) {
|
||||
log.info("加入准备区...");
|
||||
await assertRegionDisappearing(findPrepareMsg, "等待加入准备区提示消失超时");
|
||||
clickToPrepare();
|
||||
}
|
||||
},
|
||||
{ maxAttempts: 60 }
|
||||
);
|
||||
//! 关闭游戏说明对话框
|
||||
await assertRegionDisappearing(
|
||||
findCloseDialog,
|
||||
"关闭游戏说明对话框超时",
|
||||
() => {
|
||||
findCloseDialog()?.click();
|
||||
},
|
||||
{ maxAttempts: 10, retryInterval: 500 }
|
||||
);
|
||||
//! 执行随机通关回放文件
|
||||
await execStagePlayback();
|
||||
await sleep(3e3);
|
||||
//! 退出关卡返回大厅
|
||||
await exitStageToLobby();
|
||||
};
|
||||
//! 执行通关回放文件(随机抽取)
|
||||
var execStagePlayback = async () => {
|
||||
const list = availablePlaybackFiles();
|
||||
const file2 = list[Math.floor(Math.random() * list.length)];
|
||||
log.info("执行通关回放文件: {file}", file2);
|
||||
await keyMouseScript.runFile(file2);
|
||||
};
|
||||
//! 退出关卡
|
||||
var exitStage = async () => {
|
||||
if (findStageEscBtn() === void 0) return;
|
||||
log.warn("关卡超时,尝试退出关卡...");
|
||||
await assertRegionAppearing(
|
||||
findExitStageBtn,
|
||||
"等待中断挑战按钮出现超时",
|
||||
() => {
|
||||
keyPress("VK_ESCAPE");
|
||||
},
|
||||
{ maxAttempts: 5, retryInterval: 2e3 }
|
||||
);
|
||||
findExitStageBtn()?.click();
|
||||
await genshin.returnMainUi();
|
||||
};
|
||||
//! 退出关卡返回大厅
|
||||
var exitStageToLobby = async () => {
|
||||
if (isInLobby()) {
|
||||
log.warn("已处于奇域大厅,跳过");
|
||||
return;
|
||||
}
|
||||
log.info("退出关卡返回大厅...");
|
||||
const done = await waitForAction(
|
||||
isInLobby,
|
||||
async () => {
|
||||
//! 跳过奇域等级提升页面(奇域等级每逢11、21、31、41级时出现加星页面)
|
||||
findSkipLevelUpMsg()?.click();
|
||||
//! 点击底部 “返回大厅” 按钮
|
||||
findBottomBtnText("大厅", true)?.click();
|
||||
},
|
||||
{ maxAttempts: 60 }
|
||||
);
|
||||
if (!done) {
|
||||
await exitStage();
|
||||
throw new Error("退出关卡返回大厅超时");
|
||||
}
|
||||
};
|
||||
|
||||
// main.ts
|
||||
(async function() {
|
||||
//! 初始化游戏环境
|
||||
setGameMetrics(1920, 1080, 1.5);
|
||||
await genshin.returnMainUi();
|
||||
//! 确保通关回放文件存在
|
||||
ensurePlaybackFilesExist();
|
||||
//! 初始化数据存储
|
||||
const store = useStoreWithDefaults("data", {
|
||||
weekly: { expGained: 0, attempts: 0 },
|
||||
nextWeek: getNextMonday4AM().getTime()
|
||||
});
|
||||
//! 新的一周开始,重置经验值数据
|
||||
if (Date.now() >= store.nextWeek) {
|
||||
store.weekly = { expGained: 0, attempts: 0 };
|
||||
store.nextWeek = getNextMonday4AM().getTime();
|
||||
}
|
||||
//! 检查本周经验值是否已达上限
|
||||
if (store.weekly.expGained >= userConfig.expWeeklyLimit) {
|
||||
if (userConfig.force) {
|
||||
log.warn("本周获取经验值已达上限,强制执行");
|
||||
} else {
|
||||
log.warn("本周获取经验值已达上限,跳过执行");
|
||||
return;
|
||||
}
|
||||
}
|
||||
//! 计算本次本周剩余可获取经验值
|
||||
let expRemaining = userConfig.expWeeklyLimit - store.weekly.expGained;
|
||||
expRemaining = expRemaining > 0 ? expRemaining : userConfig.expWeeklyLimit;
|
||||
//! 计算需要进行的尝试次数
|
||||
let attempts = Math.ceil(expRemaining / userConfig.expPerAttempt);
|
||||
attempts = userConfig.thisAttempts > 0 ? userConfig.thisAttempts : attempts;
|
||||
//! 离开当前所在房间(如果存在)
|
||||
await leaveRoom();
|
||||
//! 创建进度追踪器
|
||||
const tracker = new ProgressTracker(attempts);
|
||||
//! 迭代尝试
|
||||
try {
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
tracker.print(`开始本周第 ${store.weekly.attempts + 1} 次奇域挑战...`);
|
||||
//! 删除关卡存档
|
||||
await deleteStageSave();
|
||||
//! 进入房间
|
||||
await enterRoom(userConfig.room);
|
||||
//! 游玩关卡
|
||||
await playStage();
|
||||
//! 关卡结束,更新数据存储
|
||||
store.weekly.attempts += 1;
|
||||
store.weekly.expGained += userConfig.expPerAttempt;
|
||||
tracker.tick({ increment: 1 });
|
||||
//! 本周已获取经验值达到上限,跳出循环
|
||||
if (store.weekly.expGained >= userConfig.expWeeklyLimit) {
|
||||
if (!userConfig.force) {
|
||||
log.warn("本周已获取经验值达到上限,停止执行");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
//! 发生主机异常(如:任务取消异常等),无法再继续执行
|
||||
if (isHostException(err)) throw err;
|
||||
//! 发生脚本流程异常,尝试退出关卡(如果在关卡中)
|
||||
await exitStage();
|
||||
log.error("脚本执行出错: {error}", getErrorMessage(err));
|
||||
}
|
||||
//! 返回提瓦特大陆
|
||||
await exitLobbyToTeyvat();
|
||||
})();
|
||||
19
repo/js/MiliastraExperiencePlayback/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "千星奇域·每周经验刷取(回放通关版)",
|
||||
"version": "0.1.0",
|
||||
"bgi_version": "0.53.0",
|
||||
"description": "千星奇域·每周经验刷取(回放通关版)",
|
||||
"authors": [
|
||||
{
|
||||
"name": "breadgrocery",
|
||||
"link": "https://github.com/breadgrocery/miliastra-experience-playback"
|
||||
}
|
||||
],
|
||||
"main": "main.js",
|
||||
"settings_ui": "settings.json",
|
||||
"saved_files": [
|
||||
"store/*.json",
|
||||
"playbacks/*.json"
|
||||
]
|
||||
}
|
||||
56
repo/js/MiliastraExperiencePlayback/settings.json
Normal file
@@ -0,0 +1,56 @@
|
||||
[
|
||||
{
|
||||
"type": "input-text",
|
||||
"name": "room",
|
||||
"label": "奇域关卡关键词或关卡GUID",
|
||||
"default": "20134075027"
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"name": "playbacks",
|
||||
"label": "通关回放文件池(自行录制拷贝到assets/playbacks,逗号分隔)",
|
||||
"default": "通关回放1.json,通关回放2.json"
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"name": "expPerAttempt",
|
||||
"label": "每次通关获取的经验值(如果勾选删除关卡存档,请自行增加)",
|
||||
"default": "20"
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"name": "deleteStageSave",
|
||||
"label": "删除关卡存档(可重复达成成就,获取更多经验值)",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"name": "deleteStageSaveKeyword",
|
||||
"label": "删除关卡存档关键字(关卡存档视图中的[关卡]列)",
|
||||
"default": "深渊100层"
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"name": "expWeeklyLimit",
|
||||
"label": "每周可获取的经验值上限",
|
||||
"default": "4000"
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"name": "force",
|
||||
"label": "忽略本周经验值已达上限",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"name": "thisAttempts",
|
||||
"label": "指定通关次数(0表示自动判断)",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"name": "goToTeyvat",
|
||||
"label": "完成后返回提瓦特大陆",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||