@@ -12,14 +12,27 @@ const DEFAULT_OCR_TIMEOUT_SECONDS = 10;
const DEFAULT _FIGHT _TIMEOUT _SECONDS = 120 ;
const DEFAULT _SWIM _CONSECUTIVE _LIMIT = 5 ;
const ERROR _CODES = {
BATTLE _TIMEOUT : "BATTLE_TIMEOUT" ,
SWIM _LIMIT : "SWIM_LIMIT" ,
SWIM _RECOVERED : "SWIM_RECOVERED" ,
} ;
const ERR _MESSAGES = {
BATTLE _TIMEOUT : "战斗超时,未检测到结果" ,
SWIM _RECOVERED : "检测到游泳且自动回七天神像,视为本轮失败" ,
} ;
// 掉落检测状态
// detectedExpOrMora: 上一轮是否识别到经验/摩拉图标(用于判断“连续两轮都没有掉落则停”)
// N oExpOrMoraCount: 连续“没有识别到掉落”的计数器
// n oExpOrMoraCount: 连续“没有识别到掉落”的计数器
// running: 后台循环 detectExpOrMora 的停止开关(必须在任何退出路径上关闭,避免后台占用)
let detectedExpOrMora = true ;
let N oExpOrMoraCount = 0 ;
let n oExpOrMoraCount = 0 ;
let running = true ;
const warnedEnemyTypes = new Set ( ) ;
// 默认突发任务 OCR 关键词(敌人配置未提供时使用)
const DEFAULT _OCR _KEYWORDS = [ "突发" , "任务" , "打倒" , "消灭" , "敌人" , "所有" ] ;
@@ -84,8 +97,12 @@ moraRo.Threshold = 0.85;
moraRo . Use3Channels = true ;
moraRo . InitTemplate ( ) ;
/**
* 脚本入口:读取配置并执行主循环;退出时停止后台检测并释放模板资源。
*/
( async function ( ) {
const startTime = Date . now ( ) ;
let enemyType = "盗宝团" ;
try {
// 可选:启用自动拾取实时任务
if ( convertToTrueIfNotBoolean ( settings . pickupMode ) ) {
@@ -95,11 +112,11 @@ moraRo.InitTemplate();
log . info ( "已 禁用 自动拾取任务" ) ;
}
// 修复点: runTimes 必须用 let/const 声明,避免污染全局变量
let runTimes = await calulateRunTimes ( ) ;
let runTimes = await calc ulateRunTimes ( ) ;
await switchPartyIfNeeded ( settings . partyName ) ;
// 选择敌人类型(默认盗宝团)
const enemyType = settings . enemyType || "盗宝团" ;
enemyType = settings . enemyType || "盗宝团" ;
log . info ( ` 当前选择的敌人类型: ${ enemyType } ` ) ;
log . info ( ` ${ enemyType } 好感开始... ` ) ;
@@ -114,7 +131,7 @@ moraRo.InitTemplate();
log . info ( ` ${ enemyType } 好感运行总时长: ${ LogTimeTaken ( startTime ) } ` ) ;
} catch ( error ) {
if ( isCancellationError ( error ) ) {
resetDailySwim ConsecutiveIfNeeded( ) ;
SwimTracker ( enemyType ) . reset ConsecutiveIfNeeded( ) ;
log . info ( "脚本已取消" ) ;
return ;
}
@@ -123,23 +140,63 @@ moraRo.InitTemplate();
} finally {
// 修复点:无论成功/失败/异常,都必须停止后台循环与释放模板资源
running = false ;
try { expMat ? . Dispose ? . ( ) ; } catch { }
try { moraMat ? . Dispose ? . ( ) ; } catch { }
safeDispose ( expMat ) ;
safeDispose ( moraMat ) ;
expMat = null ;
moraMat = null ;
}
} ) ( ) ;
/**
* 将 settings 中非布尔值按默认 true 处理(未设置视为开启)。
* @param {*} value
* @returns {boolean}
*/
function convertToTrueIfNotBoolean ( value ) {
// settings 里可能是 undefined/字符串等非布尔值,此处按“未设置则视为开启”处理
return typeof value === 'boolean' ? value : true ;
}
/**
* 判断是否为取消任务导致的错误。
* @param {*} error
* @returns {boolean}
*/
function isCancellationError ( error ) {
const msg = error && error . message ? String ( error . message ) : "" ;
return msg . includes ( "取消自动任务" ) || msg . includes ( "A task was canceled." ) ;
}
/**
* 安全释放宿主对象资源(兼容 Dispose/dispose) 。
* @param {*} obj
*/
function safeDispose ( obj ) {
if ( ! obj ) return ;
const fn =
( typeof obj . Dispose === "function" ? obj . Dispose : null ) ||
( typeof obj . dispose === "function" ? obj . dispose : null ) ;
if ( ! fn ) return ;
try { fn . call ( obj ) ; } catch { }
}
/**
* 创建带错误码的 Error, 便于上层结构化处理。
* @param {string} code
* @param {string} message
* @returns {Error}
*/
function createScriptError ( code , message ) {
const err = new Error ( message ) ;
err . code = code ;
return err ;
}
/**
* 判断 CancellationTokenSource 是否已请求取消(兼容不同字段命名)。
* @param {*} cts
* @returns {boolean}
*/
function isCtsCancellationRequested ( cts ) {
try {
const v =
@@ -153,6 +210,10 @@ function isCtsCancellationRequested(cts) {
}
}
/**
* 获取地图坐标,失败时返回 null( 取消错误会透传) 。
* @returns {{x:number,y:number}|null}
*/
function safeGetPositionFromMap ( ) {
try {
return genshin . getPositionFromMap ( ) ;
@@ -164,11 +225,26 @@ function safeGetPositionFromMap() {
}
}
/**
* 获取敌人配置;未知 enemyType 仅提示一次并回退为空配置。
* @param {string} enemyType
* @returns {object}
*/
function getEnemyConfig ( enemyType ) {
// 根据敌人类型返回配置对象(不存在则返回空对象,调用方负责兜底)
return ENEMY _CONFIG [ enemyType ] || { } ;
const cfg = ENEMY _CONFIG [ enemyType ] ;
if ( ! cfg && ! warnedEnemyTypes . has ( enemyType ) ) {
warnedEnemyTypes . add ( enemyType ) ;
log . warn ( ` 未知 enemyType: ${ enemyType } ,将使用默认配置 ` ) ;
}
return cfg || { } ;
}
/**
* 敌人准备流程:执行 preparePath / 盗宝团可选清理丘丘人。
* @param {string} enemyType
* @returns {Promise<void>}
*/
async function prepareForEnemy ( enemyType ) {
// 清理丘丘人(仅盗宝团需要)
if ( settings . qiuQiuRen && enemyType === "盗宝团" ) {
@@ -183,6 +259,11 @@ async function prepareForEnemy(enemyType) {
}
}
/**
* 战后附加流程:按敌人配置跑 postBattlePath; 蕈兽包含对话交互。
* @param {string} enemyType
* @returns {Promise<void>}
*/
async function runPostBattle ( enemyType ) {
// 战斗后处理:按敌人配置执行附加路径;部分敌人需要对话交互
const { postBattlePath } = getEnemyConfig ( enemyType ) ;
@@ -201,6 +282,12 @@ async function runPostBattle(enemyType) {
}
}
/**
* 失败恢复:回七天神像(可跳过)并走回退路径。
* @param {string} enemyType
* @param {boolean} [skipTp=false]
* @returns {Promise<void>}
*/
async function recoverAfterFailure ( enemyType , skipTp = false ) {
if ( ! skipTp ) {
await genshin . tpToStatueOfTheSeven ( ) ;
@@ -218,6 +305,11 @@ async function recoverAfterFailure(enemyType, skipTp = false) {
}
}
// 执行 path 任务
/**
* 执行路径文件( AutoPath/*.json) ; 失败返回 false, 取消错误透传。
* @param {string} locationName
* @returns {Promise<boolean>}
*/
async function AutoPath ( locationName ) {
// 统一包装路径执行:避免 runFile 抛错导致整个脚本中断
try {
@@ -233,6 +325,10 @@ async function AutoPath(locationName) {
}
}
/**
* 生成当天日期键( YYYY-MM-DD) 。
* @returns {string}
*/
function getTodayKey ( ) {
const d = new Date ( ) ;
const yyyy = String ( d . getFullYear ( ) ) ;
@@ -241,11 +337,21 @@ function getTodayKey() {
return ` ${ yyyy } - ${ mm } - ${ dd } ` ;
}
/**
* 统一路径字符串用于比较(反斜杠转正斜杠并去重)。
* @param {string} p
* @returns {string}
*/
function normalizePathForCompare ( p ) {
if ( ! p || typeof p !== "string" ) return "" ;
return p . replace ( /\\/g , "/" ) . replace ( /\/+/g , "/" ) ;
}
/**
* 安全判断文件是否存在(宿主缺少 exists API 时的兜底实现)。
* @param {string} filePath
* @returns {boolean}
*/
function fileExistsSafe ( filePath ) {
try {
if ( ! filePath || typeof filePath !== "string" ) return false ;
@@ -269,57 +375,95 @@ function fileExistsSafe(filePath) {
}
}
function loadDailySwimStats ( ) {
/**
* 游泳状态追踪(仅盗宝团启用):记录连续触发次数并支持阈值中断。
* @param {string} enemyType
* @returns {{enabled:boolean, load:Function, resetConsecutiveIfNeeded:Function, recordAndCheck:Function}}
*/
function SwimTracker ( enemyType ) {
const enabled = enemyType === "盗宝团" ;
const statsPath = "swim_stats.json" ;
const today = getTodayKey ( ) ;
const statsPath = "swim_stats.json" ;
try {
if ( ! fileExistsSafe ( statsPath ) ) {
return { date : today , totalSwimCount : 0 , consecutiveSwimCount : 0 } ;
}
const raw = file . readTextSync ( statsPath ) ;
const parsed = JSON . parse ( raw ) ;
if ( parsed && parsed . date === today ) {
const totalSwimCount = typeof parsed . totalSwimCount === "number"
? parsed . totalSwimCount
: ( typeof parsed . swimCount === "number" ? parsed . swimCount : 0 ) ;
const consecutiveSwimCount = typeof parsed . consecutiveSwimCount === "number" ? parsed . consecutiveSwimCount : 0 ;
return { date : today , totalSwimCount , consecutiveSwimCount } ;
}
} catch { }
return { date : today , totalSwimCount : 0 , consecutiveSwimCount : 0 } ;
}
function saveDailySwimStats ( stats ) {
const statsPath = "swim_stats.json" ;
try {
file . writeTextSync ( statsPath , JSON . stringify ( stats , null , 2 ) , false ) ;
} catch { }
}
function resetDailySwimConsecutiveIfNeeded ( ) {
const stats = loadDailySwimStats ( ) ;
if ( stats . consecutiveSwimCount > 0 ) {
stats . consecutiveSwimCount = 0 ;
saveDailySwimStats ( stats ) ;
/**
* 读取当天统计。
* @returns {{date:string,totalSwimCount:number,consecutiveSwimCount:number}}
*/
function load ( ) {
if ( ! enabled ) return { date : today , totalSwimCount : 0 , consecutiveSwimCount : 0 } ;
try {
if ( ! fileExistsSafe ( statsPath ) ) {
return { date : today , totalSwimCount : 0 , consecutiveSwimCount : 0 } ;
}
const raw = file . readTextSync ( statsPath ) ;
const parsed = JSON . parse ( raw ) ;
if ( parsed && parsed . date === today ) {
const totalSwimCount = typeof parsed . totalSwimCount === "number"
? parsed . totalSwimCount
: ( typeof parsed . swimCount === "number" ? parsed . swimCount : 0 ) ;
const consecutiveSwimCount = typeof parsed . consecutiveSwimCount === "number" ? parsed . consecutiveSwimCount : 0 ;
return { date : today , totalSwimCount , consecutiveSwimCount } ;
}
} catch { }
return { date : today , totalSwimCount : 0 , consecutiveSwimCount : 0 } ;
}
}
function recordSwimEventAndCheck ( consecutiveLimit ) {
const stats = loadDailySwimStats ( ) ;
stats . totalSwimCount = ( Number ( stats . totalSwimCount ) || 0 ) + 1 ;
stats . consecutiveSwimCount = ( Number ( stats . consecutiveSwimCount ) || 0 ) + 1 ;
saveDailySwimStats ( stats ) ;
const limit = Number . isFinite ( Number ( consecutiveLimit ) ) ? Number ( consecutiveLimit ) : DEFAULT _SWIM _CONSECUTIVE _LIMIT ;
return {
totalSwimCount : stats . totalSwimCount ,
consecutiveSwimCount : stats . consecutiveSwimCount ,
consecutiveLimit : limit ,
exceeded : stats . consecutiveSwimCount >= limit
} ;
/**
* 保存统计(仅启用时生效)。
* @param {{date:string,totalSwimCount:number,consecutiveSwimCount:number}} stats
*/
function save ( stats ) {
if ( ! enabled ) return ;
try {
file . writeTextSync ( statsPath , JSON . stringify ( stats , null , 2 ) , false ) ;
} catch { }
}
/**
* 若连续计数大于 0 则清零。
*/
function resetConsecutiveIfNeeded ( ) {
if ( ! enabled ) return ;
const stats = load ( ) ;
if ( stats . consecutiveSwimCount > 0 ) {
stats . consecutiveSwimCount = 0 ;
save ( stats ) ;
}
}
/**
* 记录一次游泳事件并检查是否超过阈值。
* @param {number|string} consecutiveLimit
* @returns {{totalSwimCount:number,consecutiveSwimCount:number,consecutiveLimit:number,exceeded:boolean}}
*/
function recordAndCheck ( consecutiveLimit ) {
if ( ! enabled ) {
const limit = Number . isFinite ( Number ( consecutiveLimit ) ) ? Number ( consecutiveLimit ) : DEFAULT _SWIM _CONSECUTIVE _LIMIT ;
return { totalSwimCount : 0 , consecutiveSwimCount : 0 , consecutiveLimit : limit , exceeded : false } ;
}
const stats = load ( ) ;
stats . totalSwimCount = ( Number ( stats . totalSwimCount ) || 0 ) + 1 ;
stats . consecutiveSwimCount = ( Number ( stats . consecutiveSwimCount ) || 0 ) + 1 ;
save ( stats ) ;
const limit = Number . isFinite ( Number ( consecutiveLimit ) ) ? Number ( consecutiveLimit ) : DEFAULT _SWIM _CONSECUTIVE _LIMIT ;
return {
totalSwimCount : stats . totalSwimCount ,
consecutiveSwimCount : stats . consecutiveSwimCount ,
consecutiveLimit : limit ,
exceeded : stats . consecutiveSwimCount >= limit
} ;
}
return { enabled , load , resetConsecutiveIfNeeded , recordAndCheck } ;
}
const battlePointCache = new Map ( ) ;
/**
* 从战斗点路径文件中推导最后一个坐标(缓存);失败则回退配置的 targetCoords。
* @param {string} enemyType
* @returns {{x:number,y:number}|null}
*/
function getBattlePointFromBattlePath ( enemyType ) {
if ( battlePointCache . has ( enemyType ) ) {
return battlePointCache . get ( enemyType ) ;
@@ -346,11 +490,21 @@ function getBattlePointFromBattlePath(enemyType) {
return point ;
}
async function runBattlePathAndFight ( enemyType ) {
/**
* 执行战斗点路径;可选在最后节点注入 fight action( 用于盗宝团旧流程) 。
* @param {string} enemyType
* @param {boolean} [injectFightAction=false]
* @returns {Promise<void>}
*/
async function runBattlePathToBattlePoint ( enemyType , injectFightAction = false ) {
const filePath = ` assets/AutoPath/ ${ enemyType } -战斗点.json ` ;
if ( ! fileExistsSafe ( filePath ) ) {
throw new Error ( ` 缺少战斗点路径文件: ${ filePath } ` ) ;
}
if ( ! injectFightAction ) {
await pathingScript . runFile ( filePath ) ;
return ;
}
const raw = file . readTextSync ( filePath ) ;
const parsed = JSON . parse ( raw ) ;
const positions = parsed && Array . isArray ( parsed . positions ) ? parsed . positions : null ;
@@ -374,6 +528,11 @@ async function runBattlePathAndFight(enemyType) {
}
// 计算运行时长
/**
* 计算从 startTime 到现在的耗时字符串。
* @param {number} startTimeParam
* @returns {string}
*/
function LogTimeTaken ( startTimeParam ) {
const currentTime = Date . now ( ) ;
const totalTimeInSeconds = ( currentTime - startTimeParam ) / 1000 ;
@@ -383,6 +542,13 @@ function LogTimeTaken(startTimeParam) {
}
// 计算预估时间
/**
* 估算完成时间(按平均单轮耗时线性推算)。
* @param {number} startTime
* @param {number} current
* @param {number} total
* @returns {string}
*/
function CalculateEstimatedCompletion ( startTime , current , total ) {
if ( current === 0 ) return "计算中..." ;
@@ -395,6 +561,11 @@ function CalculateEstimatedCompletion(startTime, current, total) {
}
// 检查并导航到触发点
/**
* 导航到触发点并做一次距离校验,偏离则二次导航。
* @param {string} enemyType
* @returns {Promise<void>}
*/
async function navigateToTriggerPoint ( enemyType ) {
await AutoPath ( ` ${ enemyType } -触发点 ` ) ;
const triggerPoint = getTriggerPoint ( enemyType ) ;
@@ -416,6 +587,12 @@ async function navigateToTriggerPoint(enemyType) {
}
// OCR检测突发任务
/**
* OCR 检测是否触发突发任务。
* @param {number} ocrTimeout 秒
* @param {string} enemyType
* @returns {Promise<boolean>}
*/
async function detectTaskTrigger ( ocrTimeout , enemyType ) {
// 修复点: OCR 的截图对象与 findMulti 返回对象可能持有底层资源
// 必须在 finally 中释放,避免长时间循环导致资源累积
@@ -441,8 +618,7 @@ async function detectTaskTrigger(ocrTimeout, enemyType) {
}
if ( ocrStatus ) break ;
} finally {
try { res ? . Dispose ? . ( ) ; } catch { }
try { res ? . dispose ? . ( ) ; } catch { }
safeDispose ( res ) ;
}
}
} catch ( error ) {
@@ -451,10 +627,8 @@ async function detectTaskTrigger(ocrTimeout, enemyType) {
}
log . error ( ` OCR检测突发任务过程中出错: ${ error && error . message ? error . message : error } ` ) ;
} finally {
try { resList ? . dispose ? . ( ) ; } catch { }
try { resList ? . Dispose ? . ( ) ; } catch { }
try { captureRegion ? . dispose ? . ( ) ; } catch { }
try { captureRegion ? . Dispose ? . ( ) ; } catch { }
safeDispose ( resList ) ;
safeDispose ( captureRegion ) ;
}
if ( ! ocrStatus ) {
@@ -465,65 +639,52 @@ async function detectTaskTrigger(ocrTimeout, enemyType) {
return ocrStatus ;
}
// 等待角色到达目标位置
async function waitForTargetPosition ( pathTask , targetCoords , maxWaitTime = 15000 , maxDistance = 5 ) {
const waitStartTime = Date . now ( ) ;
let isNearTarget = false ;
let pathTaskFinished = false ;
let sawPosition = false ;
// 监控路径任务完成
pathTask . then ( ( ) => {
pathTaskFinished = true ;
log . info ( "路径任务已完成" ) ;
} ) . catch ( error => {
pathTaskFinished = true ;
if ( isCancellationError ( error ) ) {
log . info ( "路径任务已取消" ) ;
return ;
}
log . error ( ` 路径任务出错: ${ error } ` ) ;
} ) ;
// 等待角色到达目标位置或超时
while ( ! isNearTarget && ! pathTaskFinished && ( Date . now ( ) - waitStartTime < maxWaitTime ) ) {
const pos = safeGetPositionFromMap ( ) ;
if ( pos ) {
sawPosition = true ;
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 ) ;
}
return { isNearTarget , pathTaskFinished , sawPosition } ;
}
// 执行战斗任务(并发执行战斗和结果检测)
async function executeBattleTasks ( fightTimeout , enemyType , cts ) {
/**
* 执行 AutoFight 并用 OCR 判定战斗结果(支持同步/异步模式与超时保护)。
* @param {number} fightTimeout 秒
* @param {string} enemyType
* @param {*} cts CancellationTokenSource
* @param {{x:number,y:number}|null} battlePointCoords
* @returns {Promise<{success:boolean,status:string,errorMessage?:string}>}
*/
async function executeBattleTasks ( fightTimeout , enemyType , cts , battlePointCoords ) {
// 修复点:
// - 以前只要“检测任务 fulfilled”就当成功; 现在明确区分 success/failure/out_of_area 等状态
// - 取消只意味着停止战斗任务,不等价于“本轮战斗成功”
log . info ( "开始战斗!" ) ;
let battleTask ;
let battleResult = null ;
let battleDetectTask = null ;
let awaitBattleTask = true ;
const awaitBattleTaskMs = 2000 ;
try {
if ( settings . disableAsyncFight ) {
// 同步战斗模式:由宿主的战斗结束检测/策略决定何时返回
await dispatcher . RunTask ( new SoloTask ( "AutoFight" ) ) ;
// 同步战斗模式:依赖配置组的“战斗结束检测”让 AutoFight 自行退出
// 额外增加 watchdog: 超过 fightTimeout 仍未退出则取消任务,避免无限战斗
const maxDetectMs = Math . max ( 0 , Number ( fightTimeout ) * 1000 ) ;
battleTask = dispatcher . RunTask ( new SoloTask ( "AutoFight" ) , cts ) ;
const battleWrapped = battleTask
. then ( value => ( { kind : "battle_fulfilled" , value } ) )
. catch ( error => ( { kind : "battle_rejected" , error } ) ) ;
const first = await Promise . race ( [
battleWrapped ,
sleep ( maxDetectMs ) . then ( ( ) => ( { kind : "battle_timeout" } ) )
] ) ;
if ( first . kind === "battle_timeout" ) {
try { cts . cancel ( ) ; } catch { }
awaitBattleTask = false ;
return { success : false , status : "auto_fight_ended" } ;
}
if ( first . kind === "battle_rejected" ) {
throw first . error ;
}
const graceMs = Math . min ( 8000 , maxDetectMs ) ;
try {
const status = await waitForBattleResult ( graceMs , enemyType , new CancellationTokenSource ( ) ) ;
const status = await waitForBattleResult ( graceMs , enemyType , cts , battlePointCoords ) ;
return { success : status === "success" , status } ;
} catch ( error ) {
if ( error && error . message && String ( error . message ) . includes ( "战斗超时" ) ) {
if ( error && ( error . code === ERROR _CODES . BATTLE _TIMEOUT || ( error . message && String ( error . message ) . includes ( "战斗超时" ) ) ) ) {
return { success : false , status : "auto_fight_ended" } ;
}
throw error ;
@@ -531,7 +692,7 @@ async function executeBattleTasks(fightTimeout, enemyType, cts) {
} else {
// 异步战斗模式:并发启动战斗 + OCR 检测结果;检测到结果后取消战斗任务
battleTask = dispatcher . RunTask ( new SoloTask ( "AutoFight" ) , cts ) ;
battleDetectTask = waitForBattleResult ( fightTimeout * 1000 , enemyType , cts ) ;
battleDetectTask = waitForBattleResult ( fightTimeout * 1000 , enemyType , cts , battlePointCoords );
const maxDetectMs = Math . max ( 0 , Number ( fightTimeout ) * 1000 ) ;
const graceMs = Math . min ( 8000 , maxDetectMs ) ;
@@ -547,13 +708,13 @@ async function executeBattleTasks(fightTimeout, enemyType, cts) {
if ( first . kind === "detect_fulfilled" ) {
log . info ( "战斗检测任务完成" ) ;
try { cts . cancel ( ) ; } catch { }
return { success : first . status === "success" , status : first . status } ;
}
if ( first . kind === "detect_rejected" ) {
throw first . error ;
}
battleResult = first ;
if ( first . kind === "battle_rejected" && isCancellationError ( first . error ) ) {
throw first . error ;
}
@@ -565,32 +726,36 @@ async function executeBattleTasks(fightTimeout, enemyType, cts) {
if ( second . kind === "detect_fulfilled" ) {
log . info ( "战斗检测任务完成" ) ;
try { cts . cancel ( ) ; } catch { }
return { success : second . status === "success" , status : second . status } ;
}
if ( second . kind === "detect_rejected" ) {
throw second . error ;
}
try { cts . cancel ( ) ; } catch { }
return { success : false , status : "auto_fight_ended" } ;
}
} catch ( error ) {
// 过滤掉正常的取消错误
if ( isCancellationError ( error ) ) {
log . info ( "战斗任务已取消" ) ;
return { success : false , status : "cancelled" } ;
}
if ( isCancellationError ( error ) ) throw error ;
const msg = error && error . message ? String ( error . message ) : "" ;
if ( msg . includes ( "前往七天神像重试" ) || msg . includes ( "检测到游泳" ) ) {
if ( SwimTracker ( enemyType ) . enabled && ( msg. includes ( "前往七天神像重试" ) || msg . includes ( "检测到游泳" ) ) ) {
log . warn ( ` 战斗执行异常(已自动回七天神像): ${ msg } ` ) ;
return { success : false , status : "recovered_to_statue" , errorMessage : msg } ;
}
log . error ( ` 战斗执行过程中出错: ${ msg } ` ) ;
return { success : false , status : "error" , errorMessage : msg } ;
} finally {
// 确保战斗任务被等待完成(即使被取消)
// 避免因 AutoFight 不响应取消导致 finally 永久挂起
if ( battleTask ) {
try {
await battleTask ;
const done = await Promise . race ( [
battleTask . then ( ( ) => true ) ,
sleep ( awaitBattleTask ? awaitBattleTaskMs : 0 ) . then ( ( ) => false )
] ) ;
if ( ! done ) {
battleTask . catch ( ( ) => { } ) ;
}
} catch ( error ) {
// 忽略 finally 块中的取消错误
if ( ! isCancellationError ( error ) ) {
@@ -603,7 +768,16 @@ async function executeBattleTasks(fightTimeout, enemyType, cts) {
}
// 执行单次好感任务循环
/**
* 执行单轮好感流程(触发检测→导航→战斗→判定→战后/恢复)。
* @param {number} roundIndex
* @param {number} ocrTimeout 秒
* @param {number} fightTimeout 秒
* @param {string} enemyType
* @returns {Promise<boolean>} 返回 false 表示整体应提前结束
*/
async function executeSingleFriendshipRound ( roundIndex , ocrTimeout , fightTimeout , enemyType ) {
const swim = SwimTracker ( enemyType ) ;
// 单轮流程:
// 1) 导航到触发点附近
// 2) 通过 OCR 判断是否已触发突发任务
@@ -618,17 +792,17 @@ async function executeSingleFriendshipRound(roundIndex, ocrTimeout, fightTimeout
}
let initialDetected = false ;
if ( roundIndex === 0 ) {
initialDetected = await detectTaskTrigger ( 3 , enemyType ) ;
initialDetected = await detectTaskTrigger ( Math . min ( Number ( ocrTimeout ) || DEFAULT _OCR _TIMEOUT _SECONDS , DEFAULT _OCR _TIMEOUT _SECONDS ) , enemyType ) ;
}
if ( ! detectedExpOrMora && settings . loopTillNoExpOrMora ) {
N oExpOrMoraCount++ ;
n oExpOrMoraCount++ ;
log . warn ( "上次运行未检测到经验或摩拉" ) ;
if ( N oExpOrMoraCount >= 2 ) {
if ( n oExpOrMoraCount >= 2 ) {
log . warn ( "连续两次循环没有经验或摩拉掉落,提前终止" ) ;
return false ;
}
} else {
N oExpOrMoraCount = 0 ;
n oExpOrMoraCount = 0 ;
detectedExpOrMora = false ;
}
if ( ! initialDetected || roundIndex > 0 ) {
@@ -645,7 +819,7 @@ async function executeSingleFriendshipRound(roundIndex, ocrTimeout, fightTimeout
// 本轮未检测到突发任务:按设计直接结束整个脚本循环
notification . send ( ` 未识别到突发任务, ${ enemyType } 好感结束 ` ) ;
log . info ( ` 未识别到突发任务, ${ enemyType } 好感结束 ` ) ;
resetDailySwim ConsecutiveIfNeeded( ) ;
swim . reset ConsecutiveIfNeeded( ) ;
return false ; // 返回 false 表示需要终止循环
}
@@ -655,40 +829,113 @@ async function executeSingleFriendshipRound(roundIndex, ocrTimeout, fightTimeout
throw new Error ( ` 未配置 ${ enemyType } 的 targetCoords ` ) ;
}
const maxDetectMs = Math . max ( 0 , Number ( fightTimeout ) * 1000 ) ;
const battle DetectCt s = new CancellationTokenSource ( ) ;
const battleDetectPromise = waitForBattleResult ( maxDetectMs , enemyType , battleDetectCts , battlePointCoords ) ;
if ( enemyType === "盗宝团" ) {
const max DetectM s = Math . max ( 0 , Number ( fightTimeout ) * 1000 ) ;
const battleDetectCts = new CancellationTokenSource ( ) ;
const battleDetectPromise = waitForBattleResult ( maxDetectMs , enemyType , battleDetectCts , battlePointCoords ) ;
log . info ( "开始战斗!" ) ;
const pathPromise = runBattlePathToBattlePoint ( enemyType , true ) ;
const pathWrapped = pathPromise
. then ( ( ) => ( { kind : "path_fulfilled" } ) )
. catch ( error => ( { kind : "path_rejected" , error } ) ) ;
const detectWrapped = battleDetectPromise
. then ( status => ( { kind : "detect_fulfilled" , status } ) )
. catch ( error => ( { kind : "detect_rejected" , error } ) ) ;
const first = await Promise . race ( [ pathWrapped , detectWrapped ] ) ;
if ( first . kind === "path_rejected" ) {
try { battleDetectCts . cancel ( ) ; } catch { }
const e = first . error ;
if ( isCancellationError ( e ) ) throw e ;
const msg = e && e . message ? String ( e . message ) : "" ;
if ( swim . enabled && ( msg . includes ( "前往七天神像重试" ) || msg . includes ( "检测到游泳" ) ) ) {
const { totalSwimCount , consecutiveSwimCount , consecutiveLimit , exceeded } = swim . recordAndCheck ( settings . swimConsecutiveLimit ) ;
log . warn ( ` 检测到游泳异常,今日累计 ${ totalSwimCount } 次,连续 ${ consecutiveSwimCount } / ${ consecutiveLimit } 次 ` ) ;
if ( exceeded ) {
throw createScriptError ( ERROR _CODES . SWIM _LIMIT , ` 当日连续触发游泳已达 ${ consecutiveLimit } 次,战斗策略或配队严重不合理 ` ) ;
}
throw createScriptError ( ERROR _CODES . SWIM _RECOVERED , ERR _MESSAGES . SWIM _RECOVERED ) ;
}
throw e ;
}
const battleStatus = first . kind === "detect_fulfilled"
? first . status
: await battleDetectPromise ;
if ( first . kind === "detect_fulfilled" ) {
const pathSettled = await Promise . race ( [
pathPromise . then ( ( ) => true ) . catch ( ( ) => true ) ,
sleep ( maxDetectMs ) . then ( ( ) => false )
] ) ;
if ( ! pathSettled ) {
throw createScriptError ( ERROR _CODES . BATTLE _TIMEOUT , ERR _MESSAGES . BATTLE _TIMEOUT ) ;
}
}
if ( battleStatus === "cancelled" ) {
throw new Error ( "战斗任务已取消" ) ;
}
if ( battleStatus === "success" ) {
swim . resetConsecutiveIfNeeded ( ) ;
await runPostBattle ( enemyType ) ;
return true ;
}
await recoverAfterFailure ( enemyType , false ) ;
throw new Error ( ` 战斗失败: ${ battleStatus } ` ) ;
}
try {
log . info ( "开始战斗!" ) ;
await runBattlePathAndFight ( enemyType ) ;
await runBattlePathToBattlePoint ( enemyType , false ) ;
} catch ( e ) {
try { battleDetectCts . cancel ( ) ; } catch { }
if ( isCancellationError ( e ) ) throw e ;
const msg = e && e . message ? String ( e . message ) : "" ;
if ( msg . includes ( "前往七天神像重试" ) || msg . includes ( "检测到游泳" ) ) {
const { totalSwimCount , consecutiveSwimCount , consecutiveLimit , exceeded } = recordSwimEvent AndCheck( settings . swimConsecutiveLimit ) ;
if ( swim . enabled && ( msg. includes ( "前往七天神像重试" ) || msg . includes ( "检测到游泳" ) ) ) {
const { totalSwimCount , consecutiveSwimCount , consecutiveLimit , exceeded } = swim . record AndCheck( settings . swimConsecutiveLimit ) ;
log . warn ( ` 检测到游泳异常,今日累计 ${ totalSwimCount } 次,连续 ${ consecutiveSwimCount } / ${ consecutiveLimit } 次 ` ) ;
if ( exceeded ) {
throw new Error ( ` 当日连续触发游泳已达 ${ consecutiveLimit } 次,战斗策略或配队严重不合理 ` ) ;
throw createScriptError ( ERROR _CODES . SWIM _LIMIT , ` 当日连续触发游泳已达 ${ consecutiveLimit } 次,战斗策略或配队严重不合理 ` ) ;
}
throw new Error ( "检测到游泳且自动回七天神像,视为本轮失败" ) ;
throw createScriptError ( ERROR _CODES . SWIM _RECOVERED , ERR _MESSAGES . SWIM _RECOVERED ) ;
}
throw e ;
}
const battleStatus = await battleDetectPromise ;
if ( battleStatus === "cancelled" ) {
throw new Error ( "战斗任务已取消" ) ;
const battleCts = new CancellationTokenSource ( ) ;
const battleResult = await executeBattleTasks ( fightTimeout , enemyType , battleCts , battlePointCoords ) ;
if ( battleResult . status === "success" ) {
swim . resetConsecutiveIfNeeded ( ) ;
await runPostBattle ( enemyType ) ;
return true ;
}
resetDailySwimConsecutiveIfNeeded ( ) ;
await runPostBattle ( enemyType ) ;
if ( battleResult . status === "recovered_to_statue" ) {
if ( swim . enabled ) {
const { totalSwimCount , consecutiveSwimCount , consecutiveLimit , exceeded } = swim . recordAndCheck ( settings . swimConsecutiveLimit ) ;
log . warn ( ` 检测到游泳异常,今日累计 ${ totalSwimCount } 次,连续 ${ consecutiveSwimCount } / ${ consecutiveLimit } 次 ` ) ;
if ( exceeded ) {
throw createScriptError ( ERROR _CODES . SWIM _LIMIT , ` 当日连续触发游泳已达 ${ consecutiveLimit } 次,战斗策略或配队严重不合理 ` ) ;
}
await recoverAfterFailure ( enemyType , true ) ;
throw createScriptError ( ERROR _CODES . SWIM _RECOVERED , ERR _MESSAGES . SWIM _RECOVERED ) ;
}
await recoverAfterFailure ( enemyType , false ) ;
throw new Error ( battleResult . errorMessage ? String ( battleResult . errorMessage ) : ` 战斗失败: ${ battleResult . status } ` ) ;
}
// 返回 true 表示成功完成这一轮
return true ;
await recoverAfterFailure ( enemyType , false ) ;
const msg = battleResult . errorMessage ? String ( battleResult . errorMessage ) : ` 战斗失败: ${ battleResult . status } ` ;
throw new Error ( msg ) ;
}
// 记录进度信息
/**
* 输出当前进度与预计完成时间。
* @param {number} startTime
* @param {number} currentRound
* @param {number} totalRounds
*/
function logProgress ( startTime , currentRound , totalRounds ) {
const estimatedCompletion = CalculateEstimatedCompletion ( startTime , currentRound + 1 , totalRounds ) ;
const currentTime = LogTimeTaken ( startTime ) ;
@@ -698,6 +945,14 @@ function logProgress(startTime, currentRound, totalRounds) {
}
// 执行 N 次好感任务并输出日志 - 重构后的主函数
/**
* 主循环:执行指定次数;在未触发/连续无掉落等条件下提前退出。
* @param {number} times
* @param {number} ocrTimeout 秒
* @param {number} fightTimeout 秒
* @param {string} enemyType
* @returns {Promise<void>}
*/
async function AutoFriendshipDev ( times , ocrTimeout , fightTimeout , enemyType = "盗宝团" ) {
// 主循环:执行指定次数或在 detectTaskTrigger 判定“未触发”时提前退出
// 修复点: finally 中统一关闭 running 并等待 detectExpOrMoraTask 退出,避免后台循环残留
@@ -725,6 +980,12 @@ async function AutoFriendshipDev(times, ocrTimeout, fightTimeout, enemyType = "
}
failureCount ++ ;
log . error ( ` 第 ${ i + 1 } 轮好感任务失败: ${ error . message } ` ) ;
if ( error && error . code === ERROR _CODES . BATTLE _TIMEOUT ) {
throw error ;
}
if ( error && error . code === ERROR _CODES . SWIM _LIMIT ) {
throw error ;
}
if ( error . message && error . message . includes ( "战斗超时" ) ) {
throw error ;
}
@@ -739,9 +1000,14 @@ async function AutoFriendshipDev(times, ocrTimeout, fightTimeout, enemyType = "
}
} finally {
if ( ! cancelled ) {
const stats = loadDailySwimStats ( ) ;
const limit = Number . isFinite ( Number ( settings . swimConsecutiveLimit ) ) ? Number ( settings . swimConsecutiveLimit ) : DEFAULT _SWIM _CONSECUTIVE _LIMIT ;
log . info ( ` 本次运行统计:成功 ${ successCount } 次,失败 ${ failureCount } 次,今日游泳累计 ${ stats . totalSwimCount } 次,连续游泳 ${ stats . consecutiveSwimCount } / ${ limit } 次 ` ) ;
const swim = SwimTracker ( enemyType ) ;
if ( swim . enabled ) {
const stats = swim . load ( ) ;
const limit = Number . isFinite ( Number ( settings . swimConsecutiveLimit ) ) ? Number ( settings . swimConsecutiveLimit ) : DEFAULT _SWIM _CONSECUTIVE _LIMIT ;
log . info ( ` 本次运行统计:成功 ${ successCount } 次,失败 ${ failureCount } 次,今日游泳累计 ${ stats . totalSwimCount } 次,连续游泳 ${ stats . consecutiveSwimCount } / ${ limit } 次 ` ) ;
} else {
log . info ( ` 本次运行统计:成功 ${ successCount } 次,失败 ${ failureCount } 次 ` ) ;
}
}
running = false ;
if ( detectExpOrMoraTask ) {
@@ -750,6 +1016,10 @@ async function AutoFriendshipDev(times, ocrTimeout, fightTimeout, enemyType = "
}
}
/**
* 后台循环:模板匹配检测经验/摩拉掉落图标(用于提前终止循环)。
* @returns {Promise<void>}
*/
async function detectExpOrMora ( ) {
// 后台循环:通过模板匹配检测经验/摩拉图标
// 注意:该循环依赖 running 停止;必须保证任何退出路径都会把 running=false
@@ -757,15 +1027,17 @@ async function detectExpOrMora() {
try { await sleep ( 1 ) ; } catch ( e ) { break ; }
let gameRegion ;
if ( ! detectedExpOrMora ) {
let res1 = null ;
let res2 = null ;
try {
gameRegion = captureGameRegion ( ) ;
const res1 = gameRegion . find ( expRo ) ;
res1 = gameRegion . find ( expRo ) ;
if ( res1 . isExist ( ) ) {
log . info ( "识别到经验" ) ;
detectedExpOrMora = true ;
continue ;
}
const res2 = gameRegion . find ( moraRo ) ;
res2 = gameRegion . find ( moraRo ) ;
if ( res2 . isExist ( ) ) {
log . info ( "识别到摩拉" ) ;
detectedExpOrMora = true ;
@@ -774,8 +1046,9 @@ async function detectExpOrMora() {
} catch ( e ) {
log . error ( ` 检测经验和摩拉掉落过程中出现错误 ${ e . message } ` ) ;
} finally {
try { gameRegion ? . dispose ? . ( ) ; } catch { }
try { gameRegion ? . Dispose ? . ( ) ; } catch { }
safeDispose ( res1 ) ;
safeDispose ( res2 ) ;
safeDispose ( gameRegion ) ;
}
} else {
//无需检测时额外等待200
@@ -785,7 +1058,11 @@ async function detectExpOrMora() {
}
}
async function calulateRunTimes ( ) {
/**
* 读取并校验运行次数设置,非法则回退默认值。
* @returns {Promise<number>}
*/
async function calculateRunTimes ( ) {
// 从 settings 读取次数并校验;非法则回退默认值
log . info ( ` '请确保队伍满员,并为队伍配置相应的战斗策略' ` ) ;
// 计算运行次数
@@ -801,11 +1078,21 @@ async function calulateRunTimes() {
}
// 验证输入是否是正整数
/**
* 判断是否为正整数。
* @param {*} value
* @returns {boolean}
*/
function isPositiveInteger ( value ) {
return Number . isInteger ( value ) && value > 0 ;
}
// 根据敌人类型获取OCR关键词
/**
* 获取 OCR 关键词(优先敌人配置,否则回退默认)。
* @param {string} enemyType
* @returns {string[]}
*/
function getOcrKeywords ( enemyType ) {
// OCR 关键词获取:优先使用敌人类型配置,否则回退到默认关键词
const { ocrKeywords } = getEnemyConfig ( enemyType ) ;
@@ -813,17 +1100,32 @@ function getOcrKeywords(enemyType) {
}
// 根据敌人类型获取目标战斗点坐标
/**
* 获取目标战斗点坐标配置。
* @param {string} enemyType
* @returns {{x:number,y:number}|undefined}
*/
function getTargetCoordinates ( enemyType ) {
const { targetCoords } = getEnemyConfig ( enemyType ) ;
return targetCoords ;
}
/**
* 获取触发点坐标配置。
* @param {string} enemyType
* @returns {{x:number,y:number}|undefined}
*/
function getTriggerPoint ( enemyType ) {
const { triggerPoint } = getEnemyConfig ( enemyType ) ;
return triggerPoint ;
}
// 验证日期格式
/**
* 可选切换队伍;失败则尝试回七天神像后重试。
* @param {string} partyName
* @returns {Promise<void>}
*/
async function switchPartyIfNeeded ( partyName ) {
// 可选队伍切换:为空则直接回到主界面,避免停留在菜单/对话等状态
if ( ! partyName ) {
@@ -844,7 +1146,15 @@ async function switchPartyIfNeeded(partyName) {
}
}
async function waitForBattleResult ( timeout = 2 * 60 * 1000 , enemyType = "盗宝团" , cts = new CancellationTokenSource ( ) , battlePointCoords = null ) {
/**
* OCR 轮询战斗结果: success/failure/out_of_area/cancelled; 超时抛 BATTLE_TIMEOUT。
* @param {number} timeout 毫秒
* @param {string} enemyType
* @param {*} cts CancellationTokenSource
* @param {{x:number,y:number}|null} battlePointCoords
* @returns {Promise<"success"|"failure"|"out_of_area"|"cancelled">}
*/
async function waitForBattleResult ( timeout = 2 * 60 * 1000 , enemyType = "盗宝团" , cts = null , battlePointCoords = null ) {
// 战斗结果 OCR 判定:
// - 返回 "success":识别到成功关键字/特殊条件
// - 返回 "failure":识别到失败关键字
@@ -861,15 +1171,18 @@ async function waitForBattleResult(timeout = 2 * 60 * 1000, enemyType = "盗宝
let notFind = 0 ;
while ( Date . now ( ) - fightStartTime < timeout ) {
if ( ! cts ) cts = new CancellationTokenSource ( ) ;
if ( isCtsCancellationRequested ( cts ) ) {
return "cancelled" ;
}
let capture = null ;
let result = null ;
let result2 = null ;
try {
capture = captureGameRegion ( ) ;
// 沿用最初版写死的 OCR 框( 1080p 下的“事件完成”识别区域)
let result = capture . find ( RecognitionObject . ocr ( 850 , 150 , 200 , 80 ) ) ;
let result2 = capture . find ( RecognitionObject . ocr ( 0 , 200 , 300 , 300 ) ) ;
result = capture . find ( RecognitionObject . ocr ( 850 , 150 , 200 , 80 ) ) ;
result2 = capture . find ( RecognitionObject . ocr ( 0 , 200 , 300 , 300 ) ) ;
let text = result && result . text ? String ( result . text ) : "" ;
text = text ? text . replace ( /\s+/g , "" ) : "" ;
let text2 = result2 && result2 . text ? String ( result2 . text ) : "" ;
@@ -946,8 +1259,9 @@ async function waitForBattleResult(timeout = 2 * 60 * 1000, enemyType = "盗宝
// 出错后继续循环,不进行额外嵌套处理
}
finally {
try { capture ? . dispose ? . ( ) ; } catch { }
try { capture ? . Dispose ? . ( ) ; } catch { }
safeDispose ( result ) ;
safeDispose ( result2 ) ;
safeDispose ( capture ) ;
}
// 统一的检查间隔
@@ -956,7 +1270,7 @@ async function waitForBattleResult(timeout = 2 * 60 * 1000, enemyType = "盗宝
log . warn ( "在超时时间内未检测到战斗结果" ) ;
cts . cancel ( ) ; // 取消任务
throw new Error ( "战斗超时,未检测到结果" ) ;
throw createScriptError ( ERROR _CODES . BATTLE _TIMEOUT , ERR _MESSAGES . BATTLE _TIMEOUT ) ;
}
/**