feat(js): 千星奇域每周经验刷取(回放通关版) (#2425)

This commit is contained in:
Bread Grocery
2025-12-02 19:50:59 +08:00
committed by GitHub
parent 7a17d326dc
commit d2732be6a0
13 changed files with 1076 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
# 🌌 千星奇域·每周经验刷取(回放通关版)
### 🌟 功能介绍
- 🔁 自动重复通关指定的奇域关卡。
- 🎥 依据用户预先录制的多个通关流程,随机选取一个执行通关。
- 🧺 支持删除关卡存档后重新挑战(默认关闭),从而提升刷取效率。
- 📅 自动追踪每周经验获取进度,达到上限后再次执行将自动跳过。
- 🏞️ 执行结束后自动返回提瓦特大陆,避免影响其他自动化流程。
## 📋 使用必读
- 请勿在禁止联机的区域(如银月之庭等)运行本脚本,否则将无法运行。
- 如果开启“删除关卡存档”功能, **建议提前清理无用存档** ,保持存档列表简洁,以提升查找效率、减少误差。
- 若内置的通关流程失效,请使用程序的 **[录制回放]** 功能,自行录制 **进入关卡后的通关流程**
- 录制完成后,打开录制回放所在目录,将录制的脚本拷贝到本脚本目录下的 `assets/playbacks` 文件夹中。
- 右键编辑脚本配置,根据你所录制的流程调整相关设置(包括奇域关卡、通关回放文件池、每次通关获取的经验值等)。
- 建议选择人气较高、不易下架、流程简单的单人游玩关卡进行录制(例如石头模拟器、风景打卡类、抽卡模拟器等)。
- 根据成就完成数量的不同,每次通关可额外获得 **50250** 点经验值。内置的通关流程设计相对保守,每次通关可获得 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -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
}
}

View File

@@ -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
}
}

View 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();
})();

View 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"
]
}

View 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
}
]