feat: 盗宝团好感 -> 战斗好感,添加愚人众好感

This commit is contained in:
秋云
2025-05-25 16:40:58 +08:00
parent 9707ce8c56
commit 12f439dab4
12 changed files with 336 additions and 84 deletions

View File

@@ -0,0 +1,98 @@
# 自动好感度刷取脚本 (AutoFriendshipTreasureHunters)
## 功能简介
自动化刷取角色好感度的脚本,支持盗宝团和愚人众两种敌人类型。通过自动触发和完成突发任务来获得好感度经验。
## 敌人类型支持
### 盗宝团好感
- **触发任务**: 岛上无贼 - 消灭鬼鬼祟祟的盗宝团
- **特殊功能**: 支持清理原住民(丘丘人)
- **战斗坐标**: 稻妻地区指定位置
### 愚人众好感
- **触发任务**:
- 丘丘人今晚的伙食 - 打倒所有丘丘人
- 买卖不成正义存 - 打倒愚人众与镀金旅团
- 禁止危险运输! - 打倒所有盗宝团
- **战斗坐标**: 须弥地区专门位置
## 使用前准备
### 1. BetterGI配置
- **地图追踪行走配置**:
- ✅ 勾选"只在传送点时回复"
- ✅ 勾选"允许在JsScript使用"
- ✅ 勾选"覆盖JS中的自动战斗配置"
- **战斗配置**
- **战斗策略**: 根据队伍选择合适的战斗策略文件
- **练度要求**: 建议较高练度,纯好感队可能无法击败敌人
- **重要设置**: ❌ 关闭"自动检测战斗结束"
### 3. 队伍推荐
- 至少1-2个主力输出角色
- 建议携带治疗角色保证生存
- 避免使用纯好感队伍(练度不足)
## 脚本设置
### 基础设置
- **敌人类型**: 选择"盗宝团"或"愚人众"
- **运行次数**: 设置刷取次数默认10次
- **OCR超时**: OCR检测超时时间默认30秒
- **战斗超时**: 单次战斗超时时间默认120秒
### 高级设置
- **清理原住民**: 仅盗宝团模式有效,清理丘丘人
- **队伍名称**: 指定使用的队伍配置
- **卡时间模式**: 支持按日期周期运行
## 运行流程
1. **队伍切换**: 自动切换到指定队伍
2. **准备阶段**:
- 盗宝团: 可选清理丘丘人 → 前往触发点
- 愚人众: 直接前往触发点
3. **任务检测**: 使用OCR检测突发任务触发
4. **战斗执行**: 自动导航到战斗点并开始战斗
5. **结果判定**: 检测战斗结果并记录
6. **循环执行**: 重复上述流程直到完成设定次数
## 注意事项
### ⚠️ 重要提醒
- 脚本运行时请勿手动操作游戏
- 确保网络稳定避免OCR检测失败
- 建议在非高峰时段运行,减少延迟影响
### 🔧 故障排除
- **OCR检测失败**: 检查游戏界面是否清晰调整OCR超时时间
- **战斗失败**: 检查队伍配置和战斗策略,提升角色练度
- **路径错误**: 确保相关路径文件存在于assets/AutoPath目录
### 📊 效率优化
- 使用高练度队伍可提高战斗效率
- 合理设置超时时间避免卡死
- 选择合适的敌人类型以匹配队伍配置
## 文件依赖
- `main.js`: 主要脚本逻辑
- `settings.json`: 配置文件
- `assets/AutoPath/`: 路径文件目录
- `盗宝团-准备.json`: 盗宝团准备阶段路径,清理丘丘人
- `盗宝团-触发点.json`: 盗宝团触发点路径
- `盗宝团-战斗点.json`: 盗宝团战斗点路径
- `愚人众-准备.json`: 愚人众准备阶段路径,从传送点到触发点
- `愚人众-触发点.json`: 愚人众触发点路径
- `愚人众-战斗点.json`: 愚人众战斗点路径
## 更新日志
- **1.3**: 新增愚人众支持,优化敌人类型切换
- **1.2**: 战斗触发检测,异步检测战斗结束
- **1.1**: 盗宝团重登功能优化
- **1.0**: 基础盗宝团好感度刷取功能

View File

@@ -0,0 +1,42 @@
{
"info": {
"name": "愚人众-准备",
"type": "collect",
"author": "秋云",
"version": "1.0",
"description": "",
"map_name": "Teyvat",
"bgi_version": "0.45.0",
"tags": [],
"last_modified_time": 1748154261518
},
"positions": [
{
"id": 1,
"x": 4790.77,
"y": -3182.48,
"action": "",
"move_mode": "walk",
"action_params": "",
"type": "teleport"
},
{
"id": 2,
"x": 4771.75,
"y": -3135.45,
"action": "",
"move_mode": "dash",
"action_params": "",
"type": "path"
},
{
"id": 3,
"x": 4783.79,
"y": -3065.62,
"action": "",
"move_mode": "dash",
"action_params": "",
"type": "path"
}
]
}

View File

@@ -0,0 +1,24 @@
{
"info": {
"name": "愚人众-战斗点",
"type": "collect",
"author": "秋云",
"version": "1.0",
"description": "",
"map_name": "Teyvat",
"bgi_version": "0.45.0",
"tags": [],
"last_modified_time": 1748154217767
},
"positions": [
{
"id": 1,
"x": 4840.55,
"y": -3078.01,
"type": "path",
"move_mode": "dash",
"action": "",
"action_params": ""
}
]
}

View File

@@ -0,0 +1,24 @@
{
"info": {
"name": "愚人众-触发点",
"type": "collect",
"author": "秋云",
"version": "1.0",
"description": "",
"map_name": "Teyvat",
"bgi_version": "0.45.0",
"tags": [],
"last_modified_time": 1748154261518
},
"positions": [
{
"id": 1,
"x": 4783.79,
"y": -3065.62,
"action": "exit_and_relogin",
"move_mode": "dash",
"action_params": "",
"type": "path"
}
]
}

View File

@@ -0,0 +1,44 @@
{
"info": {
"name": "盗宝团-准备",
"type": "collect",
"author": "HZYgrandma",
"version": "1.0",
"description": "",
"bgi_version": "0.42.0"
},
"positions": [
{
"id": 1,
"x": -2740.6,
"y": -3410.69,
"action": "",
"move_mode": "walk",
"type": "teleport"
},
{
"id": 2,
"x": -2739.48,
"y": -3437.85,
"action": "",
"move_mode": "dash",
"type": "path"
},
{
"id": 3,
"x": -2764.44,
"y": -3465.81,
"action": "",
"move_mode": "dash",
"type": "path"
},
{
"id": 4,
"x": -2771.27,
"y": -3453.23,
"action": "fight",
"move_mode": "dash",
"type": "path"
}
]
}

View File

@@ -0,0 +1,20 @@
{
"info": {
"name": "盗宝团-战斗点",
"type": "collect",
"author": "HZYgrandma",
"version": "1.0",
"description": "",
"bgi_version": "0.43.4"
},
"positions": [
{
"id": 1,
"x": -2756.67,
"y": -3467.63,
"type": "path",
"move_mode": "dash",
"action": ""
}
]
}

View File

@@ -0,0 +1,36 @@
{
"info": {
"name": "触发点",
"type": "collect",
"author": "HZYgrandma",
"version": "1.0",
"description": "",
"bgi_version": "0.43.4"
},
"positions": [
{
"id": 1,
"action": "",
"move_mode": "walk",
"type": "teleport",
"x": -2738.38,
"y": -3414.45
},
{
"id": 2,
"x": -2739.72,
"y": -3400.15,
"type": "path",
"move_mode": "walk",
"action": "exit_and_relogin"
},
{
"id": 4,
"x": -2738.67,
"y": -3427.38,
"type": "path",
"move_mode": "dash",
"action": ""
}
]
}

View File

@@ -0,0 +1,380 @@
const DEFAULT_RUNS = 10;
const DEFAULT_PERIOD = 25;
const DEFAULT_BASE_RUNS = 50;
const BENCHMARK_HOUR = "T04:00:00";
const DEFAULT_OCR_TIMEOUT_SECONDS = 30;
const DEFAULT_FIGHT_TIMEOUT_SECONDS = 120;
(async function () {
// 启用自动拾取的实时任务
const startTime = Date.now();
dispatcher.addTimer(new RealtimeTimer("AutoPick"));
log.info(`'请确保队伍满员,并为队伍配置相应的战斗策略'`);
// 计算运行次数
let runTimes = Number(settings.runTimes);
if(!isPositiveInteger(runTimes) && !settings.waitTimeMode){
log.warn("请输入正确的次数,必须是正整数!");
log.warn(`运行次数重置为 ${DEFAULT_RUNS} 次!`);
runTimes = DEFAULT_RUNS;
}
if(settings.waitTimeMode){
if(!isPositiveInteger(runTimes)){
log.warn("运行次数必须是正整数,使用默认基准次数");
log.warn(`运行次数重置为 ${DEFAULT_BASE_RUNS} 次!`);
runTimes = DEFAULT_BASE_RUNS;
}
// 验证日期格式
const waitTimeModeDay = settings.waitTimeModeDay;
if (!isValidDateFormat(waitTimeModeDay)) {
log.error("基准日期格式错误,请检查后重试!");
log.error("参考格式2025-01-01");
log.error(`错误输入:${waitTimeModeDay}`);
await sleep(5000);
return;
}
let period = Number(settings.waitTimeModePeriod);
if(!isPositiveInteger(period) || period > runTimes){
period = DEFAULT_PERIOD < runTimes? DEFAULT_PERIOD : runTimes;
log.warn(`卡时间模式周期必须是 1-${runTimes} 之间的正整数!使用 ${period} 作为周期`);
}
runTimes = calculateWaitModeRuns(runTimes, waitTimeModeDay, period);
// 添加日志输出,提醒用户当前使用的基准日期和周期
log.info(`当前使用的基准日期: ${waitTimeModeDay}`);
log.info(`当前使用的周期: ${period}`);
log.info(`根据基准日期和周期计算,今日运行次数: ${runTimes}`);
} else {
log.info(`当前设置的运行次数: ${runTimes}`);
}
await switchPartyIfNeeded(settings.partyName);
// 获取敌人类型设置,默认为盗宝团
const enemyType = settings.enemyType || "盗宝团";
log.info(`当前选择的敌人类型: ${enemyType}`);
log.info(`${enemyType}好感开始...`);
// 清理丘丘人(仅盗宝团需要)
if(settings.qiuQiuRen && enemyType === "盗宝团"){
log.info(`清理原住民...`);
await AutoPath('盗宝团-准备');
}
if(enemyType === "愚人众"){
log.info(`导航到愚人众触发点...`);
await AutoPath('愚人众-准备');
}
// 验证超时设置
const ocrTimeout = validateTimeoutSetting(settings.ocrTimeout, DEFAULT_OCR_TIMEOUT_SECONDS, "OCR");
const fightTimeout = validateTimeoutSetting(settings.fightTimeout, DEFAULT_FIGHT_TIMEOUT_SECONDS, "战斗");
// 好感循环开始
await AutoFriendshipDev(runTimes, ocrTimeout, fightTimeout, enemyType);
log.info(`${enemyType}好感运行总时长:${LogTimeTaken(startTime)}`);
})();
// 执行 path 任务
async function AutoPath(locationName) {
try {
const filePath = `assets/AutoPath/${locationName}.json`;
await pathingScript.runFile(filePath);
} catch (error) {
log.error(`执行 ${locationName} 路径时发生错误: ${error.message}`);
}
}
// 计算运行时长
function LogTimeTaken(startTimeParam) {
const currentTime = Date.now();
const totalTimeInSeconds = (currentTime - startTimeParam) / 1000;
const minutes = Math.floor(totalTimeInSeconds / 60);
const seconds = totalTimeInSeconds % 60;
return `${minutes}${seconds.toFixed(0).padStart(2, '0')}`;
}
// 计算预估时间
function CalculateEstimatedCompletion(startTime, current, total) {
if (current === 0) return "计算中...";
const elapsedTime = Date.now() - startTime;
const timePerTask = elapsedTime / current;
const remainingTasks = total - current;
const remainingTime = timePerTask * remainingTasks;
const completionDate = new Date(Date.now() + remainingTime);
return `${completionDate.toLocaleTimeString()} (约 ${Math.round(remainingTime / 60000)} 分钟)`;
}
// 执行 N 次好感任务并输出日志
async function AutoFriendshipDev(times, ocrTimeout, fightTimeout, enemyType = "盗宝团") {
let startFirstTime = Date.now();
for (let i = 0; i < times; i++) {
await AutoPath(`${enemyType}-触发点`);
// 启动路径导航任务
let pathTaskPromise = AutoPath(`${enemyType}-战斗点`);
// 根据敌人类型设置不同的OCR检测关键词
const ocrKeywords = getOcrKeywords(enemyType);
// OCR检测
let ocrStatus = false;
let ocrStartTime = Date.now();
while (Date.now() - ocrStartTime < ocrTimeout * 1000 && !ocrStatus) {
let captureRegion = captureGameRegion();
let resList = captureRegion.findMulti(RecognitionObject.ocr(0, 200, 300, 300));
for (let o = 0; o < resList.count; o++) {
let res = resList[o];
for (let keyword of ocrKeywords) {
if (res.text.includes(keyword)) {
ocrStatus = true;
log.info("检测到突发任务触发");
break;
}
}
if (ocrStatus) break;
}
if (!ocrStatus) {
await sleep(1000);
}
}
if(ocrStatus){
const cts = new CancellationTokenSource();
try { // 设置最大等待时间为15秒
const maxWaitTime = 15000;
const waitStartTime = Date.now();
// 根据敌人类型设置不同的目标坐标
const targetCoords = getTargetCoordinates(enemyType);
const maxDistance = 10; // 10米距离判定
// 等待角色到达指定位置附近
let isNearTarget = false;
let pathTaskFinished = false;
// 简单监控路径任务完成
pathTaskPromise.then(() => {
pathTaskFinished = true;
log.info("路径任务已完成");
}).catch(error => {
pathTaskFinished = true;
log.error(`路径任务出错: ${error}`);
});
// 等待角色到达目标位置或超时
while (!isNearTarget && !pathTaskFinished && (Date.now() - waitStartTime < maxWaitTime)) {
const pos = genshin.getPositionFromMap();
if (pos) {
const distance = Math.sqrt(Math.pow(pos.x - targetCoords.x, 2) + Math.pow(pos.y - targetCoords.y, 2));
if (distance <= maxDistance) {
isNearTarget = true;
log.info(`已到达目标点附近,距离: ${distance.toFixed(2)}`);
break;
}
}
await sleep(1000);
} log.info("开始战斗...");
const battleTask = dispatcher.RunTask(new SoloTask("AutoFight"), cts);
const fightResultPromise = waitForBattleResult(fightTimeout * 1000, enemyType, cts);
// 使用 Promise.all 等待两个任务完成
const [battleResult, fightResult] = await Promise.all([
battleTask.catch(error => {
return { success: false, error: error };
}),
fightResultPromise // 不捕获超时错误,让它直接抛到外层
]);
cts.cancel();
} catch (error) {
cts.cancel();
if (error.message && error.message.includes("战斗超时")) {
log.error(`战斗超时,终止整个任务: ${error.message}`);
await genshin.tpToStatueOfTheSeven(); // 超时回到七天神像终止任务
throw error; // 重新抛出超时错误,终止整个任务
}
log.error(`执行过程中出错: ${error}`);
}
const estimatedCompletion = CalculateEstimatedCompletion(startFirstTime, i + 1, times);
const currentTime = LogTimeTaken(startFirstTime);
log.info(`当前进度:${i + 1}/${times} (${((i + 1) / times * 100).toFixed(1)}%)`);
log.info(`当前运行总时长:${currentTime}`);
log.info(`预计完成时间:${estimatedCompletion}`);
} else {
notification.send(`未识别到突发任务,${enemyType}好感结束`);
log.info(`未识别到突发任务,${enemyType}好感结束`);
break;
}
}
log.info(`${enemyType}好感已完成`);
// await genshin.tpToStatueOfTheSeven(); // 虽然不知道什么原因,但是不加这句会报错
}
// 验证输入是否是正整数
function isPositiveInteger(value) {
return Number.isInteger(value) && value > 0;
}
// 根据敌人类型获取OCR关键词
function getOcrKeywords(enemyType) {
if (enemyType === "愚人众") {
return ["买卖", "不成", "正义存", "愚人众","禁止","危险","运输","打倒","盗宝团"];
}
else if (enemyType === "盗宝团") {
return ["岛上", "无贼", "消灭", "鬼鬼祟祟", "盗宝团"];
}
}
// 根据敌人类型获取目标战斗点坐标
function getTargetCoordinates(enemyType) {
if (enemyType === "愚人众") {
// 愚人众战斗点坐标(需要根据实际位置调整)
return { x: 4840.55, y: -3078.01 }; // 这里需要替换为实际的愚人众战斗点坐标
} else {
// 盗宝团战斗点坐标
return { x: -2756.67, y: -3467.63 };
}
}
// 验证日期格式
function isValidDateFormat(dateStr) {
if (!dateStr) return false;
// 检查格式是否为 YYYY-MM-DD
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateStr)) return false;
// 检查是否为有效日期
const date = new Date(dateStr);
return !isNaN(date.getTime());
}
function calculateWaitModeRuns(baseRuns, waitTimeModeDay, period) {
const now = new Date();
const benchmark = new Date(waitTimeModeDay + BENCHMARK_HOUR);
const timeDiff = now.getTime() - benchmark.getTime();
const daysDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
const daysNormalized = daysDiff >= 0 ? daysDiff : period - (Math.abs(daysDiff) % period);
const dayInCycle = (daysNormalized % period) + 1;
const baseRunsPerDay = Math.ceil(baseRuns / period);
return baseRunsPerDay * dayInCycle;
}
async function switchPartyIfNeeded(partyName) {
if (!partyName) {
await genshin.returnMainUi();
return;
}
try {
log.info("正在尝试切换至" + partyName);
if(!await genshin.switchParty(partyName)){
log.info("切换队伍失败,前往七天神像重试");
await genshin.tpToStatueOfTheSeven();
await genshin.switchParty(partyName);
}
} catch {
log.error("队伍切换失败,可能处于联机模式或其他不可切换状态");
notification.error(`队伍切换失败,可能处于联机模式或其他不可切换状态`);
await genshin.returnMainUi();
}
}
async function waitForBattleResult(timeout = 2 * 60 * 1000, enemyType = "盗宝团", cts = new CancellationTokenSource()) {
let fightStartTime = Date.now();
const successKeywords = ["事件", "完成"];
const failureKeywords = ["失败"];
const eventKeywords = getOcrKeywords(enemyType);
let notFind = 0;
while (Date.now() - fightStartTime < timeout) {
try {
// 简化OCR检测只使用一个try-catch块
let result = captureGameRegion().find(RecognitionObject.ocr(850, 150, 200, 80));
let result2 = captureGameRegion().find(RecognitionObject.ocr(0, 200, 300, 300));
let text = result.text;
let text2 = result2.text;
// 检查成功关键词
for (let keyword of successKeywords) {
if (text.includes(keyword)) {
log.info("检测到战斗成功关键词: {0}", keyword);
log.info("战斗结果:成功");
cts.cancel(); // 取消任务
return true;
}
}
// 检查失败关键词
for (let keyword of failureKeywords) {
if (text.includes(keyword)) {
log.warn("检测到战斗失败关键词: {0}", keyword);
log.warn("战斗结果:失败,回到七天神像重试");
cts.cancel(); // 取消任务
await genshin.tpToStatueOfTheSeven();
if(enemyType=== "愚人众"){
await AutoPath('愚人众-准备');
}
return false;
}
}
// 检查事件关键词
let find = 0;
for(let keyword of eventKeywords) {
if (text2.includes(keyword)) {
find++;
}
}
if(find === 0) {
notFind++;
log.info("未检测到任务触发关键词:{0} 次", notFind);
}else{
notFind = 0;
}
if (notFind > 10) {
log.warn("不在任务触发区域,战斗失败");
cts.cancel(); // 取消任务
if(enemyType=== "愚人众"){
log.warn("回到愚人众准备点");
await AutoPath('愚人众-准备');
}
return false;
}
}
catch (error) {
log.error("OCR过程中出错: {0}", error);
// 出错后继续循环,不进行额外嵌套处理
}
// 统一的检查间隔
await sleep(1000);
}
log.warn("在超时时间内未检测到战斗结果");
cts.cancel(); // 取消任务
throw new Error("战斗超时,未检测到结果");
}
/**
* 验证超时时间设置
* @param {number|string} value - 用户设置的超时时间(秒)
* @param {number} defaultValue - 默认超时时间(秒)
* @param {string} timeoutType - 超时类型名称
* @returns {number} - 验证后的超时时间(秒)
*/
function validateTimeoutSetting(value, defaultValue, timeoutType) {
// 转换为数字
const timeout = Number(value);
// 检查是否为有效数字且大于0
if (!isFinite(timeout) || timeout <= 0) {
log.warn(`${timeoutType}超时设置无效必须是大于0的数字将使用默认值 ${defaultValue}`);
return defaultValue;
}
log.info(`${timeoutType}超时设置为 ${timeout}`);
return timeout;
}

View File

@@ -0,0 +1,19 @@
{
"manifest_version": 1,
"name": "战斗好感:自动好感度&卡时间",
"version": "1.3.0",
"tags": [
"好感",
"盗宝团",
"突发事件",
"愚人众"
],
"description": "通过战斗类突发事件刷好感度,刷盗宝团、愚人众材料,概率掉落兽肉,小怪锄地,卡时间等,请配合战斗策略使用。盗宝团部分在 HZYgrandma & 愚溪的原始脚本上改编。当前版本可能无法以任意方式拾取掉落物,如果有拾取掉落物需求,请勿更新。",
"authors": [
{
"name": "秋云"
}
],
"settings_ui": "settings.json",
"main": "main.js"
}

View File

@@ -0,0 +1,52 @@
[
{
"name": "enemyType",
"type": "select",
"label": "选择敌人类型",
"options": [
"盗宝团",
"愚人众"
],
"default": "盗宝团"
},
{
"name": "qiuQiuRen",
"type": "checkbox",
"label": "是否清理丘丘人\n【默认否仅对盗宝团有效选是会清理丘丘人之后再开始任务】"
},
{
"name": "partyName",
"type": "input-text",
"label": "(选填)需要切换的队伍名称"
},
{
"name": "runTimes",
"type": "input-text",
"label": "(选填)运行次数\n【默认10次卡时间模式默认50次】"
},
{
"name": "waitTimeMode",
"type": "checkbox",
"label": "卡时间模式\n【基于运行次数、基准日期、周期确定需要运行次数】"
},
{
"name": "waitTimeModeDay",
"type": "input-text",
"label": "卡时间模式基准日期\n【格式参考2025-01-01】"
},
{
"name": "waitTimeModePeriod",
"type": "input-text",
"label": "(选填)卡时间模式周期\n【默认25天】"
},
{
"name": "ocrTimeout",
"type": "input-text",
"label": "OCR超时时间\n【选填默认为30秒如果经常提前判定未识别到任务请适当调大】"
},
{
"name": "fightTimeout",
"type": "input-text",
"label": "战斗超时时间\n【选填默认为120秒】"
}
]