chore(js): 修改内置通关流程 (#2574)

This commit is contained in:
Bread Grocery
2025-12-26 20:46:22 +08:00
committed by GitHub
parent 3fed8ab041
commit 7747926dce
6 changed files with 123 additions and 72 deletions

View File

@@ -16,7 +16,7 @@
- 录制完成后,打开录制回放所在目录,将录制的脚本拷贝到本脚本目录下的 `assets/playbacks` 文件夹中(建议重命名)。
- 右键编辑脚本配置,根据你所录制的流程调整相关设置(包括奇域关卡、通关回放文件池、每次通关获取的经验值等)。
- 建议选择人气较高、不易下架(通关时长不是特别短的,**可参考关卡描述里的平均游玩时长**)、流程简单的单人游玩关卡进行录制(例如石头模拟器、抽卡模拟器、风景打卡地图等)。
- 根据成就完成数量的不同,每次通关可额外获得 **50250** 点经验值。内置的通关流程相对保守,每次通关可获得 20 点基础经验 + 50 点成就经验
- 每次通关根据成就完成数量的不同,每个成就可额外获得 **50** 点经验值(即每次通关最多可获得270点经验值
- ~~人有多大胆,地有多大产~~。**建议适当延长录制时的通关时间**,以降低关卡下架风险和自身运行风险。
- 含有部件礼箱类型的奖励不会领取,不会帮你做选择(如纪游只会领取经验奖励、不会领取巧趣醒转奖励)。
@@ -31,17 +31,17 @@
| 配置项 | 描述 | 备注 | 默认值 |
| ---------------------- | ---------------------------- | ------------------------------------------------------------------ | ---------------------------------------------- |
| room | 奇域关卡关键词或关卡GUID | | 20134075027 |
| playbacks | 通关回放文件池 | 逗号分隔随机抽取自行录制拷贝到assets/playbacks | 通关回放1.json,通关回放2.json |
| expPerAttempt | 每次通关获取的经验值 | 如果勾选删除关卡存档,请自行增加 | 20 |
| room | 奇域关卡关键词或关卡GUID | | 20031486040 |
| playbacks | 通关回放文件池 | 逗号分隔随机抽取自行录制拷贝到assets/playbacks | 美景音乐播放器结算.json |
| expPerAttempt | 每次通关获取的经验值 | 如果勾选删除关卡存档,每个成就加额外50经验值 | 20 |
| deleteStageSave | 删除关卡存档 | 可重复达成成就,获取更多经验值 | false |
| deleteStageSaveKeyword | 删除关卡存档关键字 | 关卡存档视图中的[关卡]列 | 深渊100层 |
| deleteStageSaveKeyword | 删除关卡存档关键字 | 关卡存档视图中的[关卡]列 | 音乐播放 |
| expWeeklyLimit | 每周可获取的经验值上限 | | 4000 |
| force | 忽略本周经验值已达上限 | | false |
| thisAttempts | 指定通关次数 | 0表示自动判断 | 0 |
| dailyEnabled | 执行每日通关任务 | 完后会领取纪游经验、日活奖励 | false |
| dailyRooms | 每日奇域关卡关键词或关卡GUID | 逗号分隔 | 24429042323,28644538672 |
| dailyPlaybacks | 每日通关回放文件随机池 | 同一关卡逗号分隔不同关卡分号分隔自行录制拷贝到assets/playbacks | 通关回放1.json,通关回放2.json;60秒按1通关.json |
| dailyPlaybacks | 每日通关回放文件随机池 | 同一关卡逗号分隔不同关卡分号分隔自行录制拷贝到assets/playbacks | 通关回放1.json,通关回放2.json;40秒按1通关.json |
| dailyLimit | 每日单个关卡通关上限 | | 1 |
| dailyForce | 忽略每日单个关卡通关上限 | | false |
| goToTeyvat | 完成后返回提瓦特大陆 | | true |

View File

@@ -173,12 +173,12 @@
{ "type": 1, "keyCode": 87, "mouseX": 0, "mouseY": 0, "time": 18328 },
{ "type": 0, "keyCode": 40, "mouseX": 0, "mouseY": 0, "time": 19985 },
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 63000 },
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 63156 },
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 63343 },
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 63515 },
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 63718 },
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 63890 }
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 41000 },
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 41156 },
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 41343 },
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 41515 },
{ "type": 0, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 41718 },
{ "type": 1, "keyCode": 49, "mouseX": 0, "mouseY": 0, "time": 41890 }
],
"info": {
"name": "",

View File

@@ -0,0 +1,22 @@
{
"macroEvents": [
{ "type": 0, "keyCode": 68, "mouseX": 0, "mouseY": 0, "time": 1094 },
{ "type": 1, "keyCode": 68, "mouseX": 0, "mouseY": 0, "time": 4578 },
{ "type": 0, "keyCode": 87, "mouseX": 0, "mouseY": 0, "time": 5438 },
{ "type": 1, "keyCode": 87, "mouseX": 0, "mouseY": 0, "time": 10750 },
{ "type": 0, "keyCode": 70, "mouseX": 0, "mouseY": 0, "time": 11781 },
{ "type": 1, "keyCode": 70, "mouseX": 0, "mouseY": 0, "time": 11891 },
{ "type": 0, "keyCode": 70, "mouseX": 0, "mouseY": 0, "time": 12781 },
{ "type": 1, "keyCode": 70, "mouseX": 0, "mouseY": 0, "time": 12891 }
],
"info": {
"name": "",
"description": "",
"x": 319,
"y": 278,
"width": 1920,
"height": 1080,
"recordDpi": 1.5
}
}

View File

@@ -5,7 +5,7 @@
* 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
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/workflow.js
var defaultMaxAttempts = 5;
var defaultRetryInterval = 1e3;
var waitForAction = async (condition, retryAction, options) => {
@@ -33,7 +33,7 @@ var waitForRegionDisappear = async (regionProvider, retryAction, options) => {
}, retryAction, options);
};
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/asserts.js
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/asserts.js
var assertRegionAppearing = async (regionProvider, message, retryAction, options) => {
const isAppeared = await waitForRegionAppear(regionProvider, retryAction, options);
if (!isAppeared) {
@@ -47,7 +47,7 @@ var assertRegionDisappearing = async (regionProvider, message, retryAction, opti
}
};
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/exception.js
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/exception.js
var getErrorMessage = (err) => {
if (err && "message" in err && typeof err.message === "string")
return err.message;
@@ -57,22 +57,22 @@ 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) => {
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/mouse.js
var simulateScroll = async (wheelDelta, times) => {
const script = {
macroEvents: Array(times).fill({ type: 6, mouseX: 0, mouseY: scrollAmountInClicks, time: 0 }),
macroEvents: Array(times).fill({ type: 6, mouseX: 0, mouseY: wheelDelta, 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)) => {
var mouseScrollDown = (height, algorithm = (h) => Math.floor(h / 17.9795)) => {
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
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/ocr.js
var findImageWithinBounds = (image, x, y, w, h, config = {}) => {
const ir = captureGameRegion();
try {
@@ -89,7 +89,7 @@ var findImageWithinBounds = (image, x, y, w, h, config = {}) => {
ir.dispose();
}
};
var findFirst = (ir, ro, predicate) => {
var findFirstRegion = (ir, ro, predicate) => {
const candidates = ir.findMulti(ro);
for (let i = 0; i < candidates.count; i++) {
if (predicate(candidates[i]))
@@ -97,19 +97,21 @@ var findFirst = (ir, ro, predicate) => {
}
return void 0;
};
var findTextWithinBounds = (text, x, y, w, h, options, config = {}) => {
var textMatch = (text, searchText, options) => {
const { ignoreCase = true, contains = false } = options || {};
const searchText = ignoreCase ? text.toLowerCase() : text;
text = ignoreCase ? text.toLowerCase() : text;
searchText = ignoreCase ? searchText.toLowerCase() : searchText;
return contains ? text.includes(searchText) : text === searchText;
};
var findTextWithinBounds = (text, x, y, w, h, options, config = {}) => {
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();
return findFirstRegion(ir, ro, (region) => {
return region.isExist() && textMatch(region.text, text, options);
});
} catch (err) {
log.warn(`${err.message || err}`);
@@ -117,34 +119,61 @@ var findTextWithinBounds = (text, x, y, w, h, options, config = {}) => {
ir.dispose();
}
};
var findTextWithinListView = async (text, listView, matchOptions, retryOptions, config = {}) => {
var findWithinListView = async (condition, listView, retryOptions, sampling, threshold = 0.9) => {
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) {
const { maxAttempts = 99, retryInterval = 1200 } = retryOptions || {};
sampling ??= (r) => r.deriveCrop(1, r.height * 0.5, r.width - 1, r.height * 0.5);
const captureListViewRegion = () => captureGameRegion().deriveCrop(x, y, w, h);
const isReachedBottom = /* @__PURE__ */ (() => {
let lastCaptured;
return () => {
const newRegion = captureListViewRegion();
if (!newRegion?.isExist())
return true;
} else {
lastTextRegion = textRegion;
return false;
try {
if (!lastCaptured)
return false;
const oldRegion = sampling(lastCaptured);
if (!oldRegion?.isExist())
return true;
const ro = RecognitionObject.templateMatch(oldRegion.srcMat);
ro.threshold = threshold;
ro.use3Channels = true;
ro.initTemplate();
return newRegion.find(ro)?.isExist();
} finally {
lastCaptured = newRegion;
}
}
return true;
};
const isTextFoundOrBottomReached = await waitForAction(() => findTargetText() != void 0 || isReachedBottom(), async () => {
};
})();
let targetRegion;
await waitForAction(() => {
targetRegion = condition(captureListViewRegion());
return targetRegion?.isExist() || isReachedBottom();
}, async () => {
moveMouseTo(x + w - paddingX, y + paddingY);
await sleep(50);
await mouseScrollDownLines(scrollLines, lineHeight);
}, { maxAttempts, retryInterval });
return isTextFoundOrBottomReached ? findTargetText() : void 0;
if (targetRegion?.isExist()) {
const { item1, item2 } = targetRegion.convertPositionToGameCaptureRegion(0, 0);
Object.assign(targetRegion, { x: item1, y: item2 });
return targetRegion;
}
};
var findTextWithinListView = async (text, listView, matchOptions, retryOptions, config = {}, sampling, threshold = 0.9) => {
const ro = RecognitionObject.ocrThis;
if (Object.keys(config).length > 0) {
Object.assign(ro, config) && ro.initTemplate();
}
return findWithinListView((lvr) => {
return findFirstRegion(lvr, ro, (region) => {
return region.isExist() && textMatch(region.text, text, matchOptions);
});
}, listView, retryOptions, sampling, threshold);
};
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/misc.js
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/misc.js
var deepMerge = (...objects) => {
const isPlainObject = (input) => input?.constructor === Object;
return objects.reduce((result, obj) => {
@@ -156,7 +185,7 @@ var deepMerge = (...objects) => {
}, {});
};
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/time.js
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/time.js
var getNextDay4AM = () => {
const now = /* @__PURE__ */ new Date();
const result = new Date(now);
@@ -189,7 +218,7 @@ 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
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/progress.js
var ProgressTracker = class {
total = 0;
current = 0;
@@ -256,7 +285,7 @@ var ProgressTracker = class {
}
};
// node_modules/.pnpm/@bettergi+utils@0.1.19/node_modules/@bettergi/utils/dist/store.js
// node_modules/.pnpm/@bettergi+utils@0.1.25/node_modules/@bettergi/utils/dist/store.js
var useStore = (name) => {
const filePath = `store/${name}.json`;
const obj = (() => {
@@ -302,27 +331,27 @@ var useStore = (name) => {
return createProxy(obj);
};
var useStoreWithDefaults = (name, defaults) => {
const store2 = useStore(name);
Object.assign(store2, deepMerge(defaults, store2));
return store2;
const newStore = useStore(name);
Object.assign(newStore, deepMerge(defaults, newStore));
return newStore;
};
// 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),
room: settings.room || "20031486040",
playbacks: (settings.playbacks || "美景音乐播放器结算.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层",
deleteStageSaveKeyword: settings.deleteStageSaveKeyword || "音乐播放",
expWeeklyLimit: Math.max(1, Number(settings.expWeeklyLimit || "4000")),
force: settings.force ?? false,
thisAttempts: Math.max(0, Number(settings.thisAttempts || "0")),
//! 每日任务相关设置
dailyEnabled: settings.dailyEnabled ?? false,
dailyRooms: (settings.dailyRooms || "24429042323,28644538672").replace(//g, ",").split(",").map((str) => str.trim()).filter(Boolean),
dailyPlaybacks: (settings.dailyPlaybacks || "通关回放1.json,通关回放2.json;60秒按1通关.json").replace(//g, ",").replace(//g, ";").split(";").map((str) => str.trim()).filter(Boolean).reduce((arr, room) => {
dailyPlaybacks: (settings.dailyPlaybacks || "通关回放1.json,通关回放2.json;40秒按1通关.json").replace(//g, ",").replace(//g, ";").split(";").map((str) => str.trim()).filter(Boolean).reduce((arr, room) => {
const files = room.split(",").map((str) => str.trim()).filter(Boolean);
if (files.length > 0) arr.push(files);
return arr;
@@ -355,7 +384,7 @@ var findBottomBtnText = (text, contains) => {
//! 通用:查找关闭对话框按钮
var findCloseDialog = () => {
const img = "assets/UI_BtnIcon_Close.png";
const iro = findImageWithinBounds(img, 480, 216, 960, 648, { useMask: true, threshold: 0.75 });
const iro = findImageWithinBounds(img, 410, 160, 1100, 660, { useMask: true, threshold: 0.8 });
iro?.drawSelf("group_img");
return iro;
};
@@ -428,7 +457,7 @@ var findEnterRoomShortcut = () => {
//! 房间:查找退出房间按钮
var findLeaveRoomBtn = () => {
const img = "assets/UI_Icon_Leave_Right.png";
const iro = findImageWithinBounds(img, 1570, 0, 350, 100);
const iro = findImageWithinBounds(img, 1570, 0, 350, 100, { threshold: 0.8 });
iro?.drawSelf("group_img");
return iro;
};
@@ -487,8 +516,8 @@ var findExternalSaveColumnPos = () => {
var findDeleteExternalSaveChecked = (colPos) => {
const img = "assets/Checkbox_Checked.png";
const iro = findImageWithinBounds(img, colPos, 250, 290, 710, {
threshold: 0.6,
use3Channels: false
threshold: 0.75,
use3Channels: true
});
iro?.drawSelf("group_img");
return iro;
@@ -542,7 +571,6 @@ var isInLobby = () => findBeyondHallBtn() !== void 0;
var isInTeyvat = () => {
return findGachaBtn() !== void 0 && findBeyondRecommendBtn() !== void 0;
};
//! 从提瓦特前往公共大厅
//! 退出大厅返回提瓦特大陆
var exitLobbyToTeyvat = async () => {
if (!userConfig.goToTeyvat) return;
@@ -637,7 +665,7 @@ var fetchCultivateReward = async () => {
keyPress("VK_F1");
await sleep(2e3);
if (findHeaderTitle("盛邀", true) === void 0) {
keyPress("VK_E");
keyPress("VK_Q");
}
},
{ maxAttempts: 10, retryInterval: 1e3 }
@@ -676,14 +704,16 @@ var goToRecommendedWonderlands = async () => {
//! 创建并进入奇域房间
var createRoom = async (room) => {
await goToRecommendedWonderlands();
log.info("打开全部奇域界面...");
log.info("打开搜索奇域界面...");
await assertRegionAppearing(
() => findHeaderTitle("搜索", true),
"打开全部奇域界面超时",
"打开搜索奇域界面超时",
() => {
findAllWonderlandsBtn()?.click();
}
);
//! 减少网络影响带来的影响
log.info("等待奇域列表加载完成...");
await sleep(1500);
//! 记录搜索前的第一个奇域名称
let iwnt;
@@ -694,7 +724,7 @@ var createRoom = async (room) => {
await sleep(500);
wi += 1;
}
if (iwnt === void 0) throw new Error("加载全部奇域列表超时");
if (iwnt === void 0) throw new Error("奇域列表加载超时");
log.info("搜索前的第一个奇域名称: {iwnt}", iwnt);
log.info("粘贴奇域关卡文本: {room}", room);
await assertRegionAppearing(findClearInputBtn, "粘贴关卡文本超时", () => {

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"name": "千星奇域·每周经验刷取(回放通关版)",
"version": "0.1.5",
"version": "0.1.6",
"bgi_version": "0.53.0",
"description": "千星奇域·每周经验刷取(回放通关版)",
"authors": [
@@ -14,7 +14,6 @@
"settings_ui": "settings.json",
"saved_files": [
"store/*.json",
"playbacks/*.json",
"assets/playbacks/*.json"
]
}

View File

@@ -3,18 +3,18 @@
"type": "input-text",
"name": "room",
"label": "每周奇域关卡关键词或关卡GUID",
"default": "20134075027"
"default": "20031486040"
},
{
"type": "input-text",
"name": "playbacks",
"label": "每周通关回放文件随机池(逗号分隔)",
"default": "通关回放1.json,通关回放2.json"
"default": "美景音乐播放器结算.json"
},
{
"type": "input-text",
"name": "expPerAttempt",
"label": "每次通关获取的经验值(如果勾选删除关卡存档,请自行增加",
"label": "每次通关获取的经验值(如果勾选删除关卡存档,每个成就加额外50经验值",
"default": "20"
},
{
@@ -27,7 +27,7 @@
"type": "input-text",
"name": "deleteStageSaveKeyword",
"label": "删除关卡存档查找关键字",
"default": "深渊100层"
"default": "音乐播放"
},
{
"type": "input-text",
@@ -63,7 +63,7 @@
"type": "input-text",
"name": "dailyPlaybacks",
"label": "每日通关回放文件随机池(同一关卡逗号分隔,不同关卡分号分隔)",
"default": "通关回放1.json,通关回放2.json;60秒按1通关.json"
"default": "通关回放1.json,通关回放2.json;40秒按1通关.json"
},
{
"type": "input-text",