js,切换武器 模糊匹配 (#665)

* js,切换武器 模糊匹配

v2.1增加模糊匹配,增加别名匹配。v2.22兼容未知角色、未知武器

* js,切换武器 模糊匹配

增加模糊匹配,增加别名匹配;极大提高识别成功率;兼容未知角色、未知武器
This commit is contained in:
JJMdzh
2025-04-29 00:08:14 +08:00
committed by GitHub
parent 2d4d17cf93
commit db323812dd
5 changed files with 2163 additions and 98 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
[
{
"单手剑": ["无锋剑", "银剑", "冷刃", "黎明神剑", "旅行剑", "吃虎鱼刀", "飞天御剑", "暗铁剑", "笛剑", "西风剑", "匣里龙吟", "铁蜂刺", "试作斩岩", "祭礼剑", "宗室长剑", "黑岩长剑", "黑剑", "腐殖之剑", "降临之剑", "暗巷闪光", "天目影打刀", "东花坊时雨", "辰砂之纺锤", "原木刀", "西福斯的月光", "笼钓瓶一心", "狼牙", "海渊终曲", "船坞长剑", "灰河渡手", "息燧之笛", "弥坚骨", "厄水之祸", "斫峰之刃", "风鹰剑", "天空之刃", "磐岩结绿", "雾切之回光", "苍古自由之誓", "波乱月白经津", "圣显之钥", "裁叶萃光", "静水流涌之辉", "有乐御簾切", "赦罪", "岩峰巡歌"]
},
{
"双手剑": ["训练大剑", "佣兵重剑", "以理服人", "铁影阔剑", "飞天大御剑", "白铁大剑", "沐浴龙血的剑", "祭礼大剑", "西风大剑", "钟剑", "试作古华", "黑岩斩刀", "宗室大剑", "白影剑", "雨裁", "螭骨剑", "雪葬的星银", "桂木斩长正", "衔珠海皇", "千岩古剑", "恶王丸", "玛海拉的水色", "饰铁之花", "聊聊棒", "森林王器", "浪影阔剑", "便携动力锯", "「究极霸王超级魔剑」", "撼地者", "硕果钩", "松籁响起之时", "狼的末路", "天空之傲", "赤角石溃杵", "无工之剑", "裁断", "山王长牙", "苇海信标", "焚曜干阳"]
},
{
"长枪": ["新手长枪", "铁尖枪", "黑缨枪", "钺矛", "白缨枪", "匣里灭辰", "试作星镰", "决斗之枪", "流月针", "黑岩刺枪", "干岩长枪", "喜多院十文字", "西风长枪", "宗室猎枪", "龙脊长枪", "断浪长鳍", "风信之锋", "「渔获」", "贯月矢", "峡湾长歌", "沙中伟贤的对答", "勘探钻机", "虹的行迹", "公义的酬报", "镇山之钉", "且住亭御咄", "和璞鸢", "天空之脊", "贯虹之槊", "护摩之杖", "薙草之稻光", "息灾", "赤月之形", "柔灯挽歌", "赤沙之杖"]
},
{
"弓箭": ["猎弓", "历练的猎弓", "鸦羽弓", "神射手之誓", "信使", "反曲弓", "弹弓", "绝弦", "西风猎弓", "弓藏", "试作澹月", "祭礼弓", "宗室长弓", "钢轮弓", "风花之颂", "幽夜华尔兹", "黑岩战弓", "暗巷猎手", "苍翠猎弓", "曚云之月", "破魔之弓", "落霞", "王下近侍", "竭泽", "烈阳之嗣", "测距规", "静谧之曲", "鹦穿之喙", "筑云", "缀花之翎", "碎链", "天空之翼", "阿莫斯之弓", "终末嗟叹之诗", "冬极白星", "猎人之径", "飞雷之弦振", "若水", "白雨心弦", "最初的大魔术", "星鹫赤羽"]
},
{
"法器": ["学徒笔记", "口袋魔导书", "魔导绪论", "讨龙英杰谭", "翡玉法球", "甲级宝珏", "异世界行记", "西风秘典", "流浪乐章", "祭礼残章", "宗室秘法录", "匣里日月", "万国诸海图谱", "黑岩绯玉", "昭心", "试作金珀", "暗巷的酒与诗", "白辰之环", "忍冬之果", "嘟嘟可故事集", "证誓之明瞳", "盈满之实", "流浪的晚星", "纯水流华", "无垠蔚蓝之歌", "遗祀玉珑", "苍纹角杯", "木棉之环", "乘浪的回旋", "天空之卷", "四风原典", "不灭月华", "千夜浮梦", "尘世之锁", "神乐之真意", "图莱杜拉的回忆", "金流监督", "碧落之珑", "冲浪时光", "万世流涌大典", "鹤鸣余音", "寝正月初睛", "溢彩心念", "祭星者之望"]
},
{
"低星": [ "无锋剑", "训练大剑", "学徒笔记", "新手长枪", "猎弓", "口袋魔导书", "银剑", "佣兵重剑", "铁尖枪", "历练的猎弓"]
},
{
"三星": [ "冷刃", "飞天御剑", "黎明神剑", "旅行剑", "暗铁剑", "以理服人", "铁影阔剑", "白铁大剑", "钺矛", "魔导绪论", "黑缨枪", "讨龙英杰谭", "翡玉法球", "异世界行记", "鸦羽弓", "甲级宝珏", "神射手之誓", "信使", "反曲弓", "弹弓"]
}
]

View File

@@ -1,30 +1,114 @@
(async function () {
// 初始化游戏窗口大小和返回主界面
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
const Character = settings.Character || "纳西妲";
const Element = settings.Element || "";
const Weapon = settings.Weapon || "试作金珀";
const pageScrollCount = Math.min(99, Math.max(0, Math.floor(Number(settings.pageScrollCount) || 2)));
const ocrRegion = { x: 1463, y: 135, width: 256, height: 32 };
const replacementMap = { "监": "盐", "卵": "卯" };
const elements = ["", "水", "草", "雷", "风", "冰", "岩", "物"];
// 获取角色、元素、武器等设置信息
const Character = settings.Character || "纳西妲"; // 默认角色
const Element = settings.Element || ""; // 默认元素
const Weapon = settings.Weapon || "试作金珀"; // 默认武器
const pageScrollCount = Math.min(99, Math.max(0, Math.floor(Number(settings.pageScrollCount) || 2))); // 页面滚动次数
const ocrRegion = { x: 1463, y: 135, width: 256, height: 32 }; // OCR识别区域
const replacementMap = { "卵": "", "姐": "妲", "去": "云", "日": "甘", "螨": "螭", "知": "矢", "钱": "钺", "础": "咄", "厘": "匣", "排": "绯", "朦": "曚", "矿": "斫", "镰": "簾", "廉": "簾", "救": "赦", "塑": "槊", "雍": "薙" }; // OCR替换映射表
const elements = [ "火", "水", "草", "雷", "风", "冰", "岩", "物"]; // 元素列表
const weaponTypeMap = {
"1": "单手剑",
"11": "双手剑",
"12": "弓箭",
"10": "法器",
"13": "长枪"
}; // 武器类型映射表
// OCR 识别函数
async function recognizeText(targetText, ocrRegion, timeout = 5000, retryInterval = 20) {
// 加载角色数据
const filePath = "assets/combat_avatar.json";
const { aliasToNameMap, nameToWeaponMap } = await loadCombatAvatarData(filePath);
if (!aliasToNameMap || !nameToWeaponMap) {
log.error("无法加载角色数据OCR 识别无法进行。");
return;
}
// 加载武器名称数据
const weaponNamesMap = await loadWeaponNames("assets/weaponName.json");
if (!weaponNamesMap) {
log.error("无法加载武器名称数据");
return;
}
// 开始执行角色路径
await CharacterPath();
// 加载角色数据
async function loadCombatAvatarData(filePath) {
try {
const jsonData = file.readTextSync(filePath);
const combatAvatarData = JSON.parse(jsonData);
const aliasToNameMap = {}; // 用于存储别名到正式名称的映射
const nameToWeaponMap = {}; // 用于存储正式名称到武器属性的映射
combatAvatarData.forEach(character => {
aliasToNameMap[character.name] = character.name; // 存储正式名称
nameToWeaponMap[character.name] = character.weapon; // 存储武器属性
character.alias.forEach(alias => {
aliasToNameMap[alias] = character.name; // 存储别名到正式名称的映射
});
});
return { aliasToNameMap, nameToWeaponMap };
} catch (error) {
log.error(`加载或解析 JSON 文件失败: ${error}`);
return null;
}
}
// 加载武器名称数据
async function loadWeaponNames(filePath) {
try {
const jsonData = file.readTextSync(filePath);
const weaponNamesData = JSON.parse(jsonData);
const weaponNamesMap = {};
// 将武器名称数据存储到一个对象中,键为武器类型,值为武器名称数组
weaponNamesData.forEach(item => {
for (const [weaponType, weaponNames] of Object.entries(item)) {
weaponNamesMap[weaponType] = weaponNames;
}
});
// log.info(`角色别名映射表: ${JSON.stringify(aliasToNameMap)}`);
// log.info(`角色武器映射表: ${JSON.stringify(nameToWeaponMap)}`);
return weaponNamesMap;
} catch (error) {
log.error(`加载武器名称文件失败: ${error}`);
return null;
}
}
// OCR识别文本
async function recognizeText(targetText, ocrRegion, aliasToNameMap, timeout = 10000, retryInterval = 20, maxAttempts = 5) {
let startTime = Date.now();
let retryCount = 0;
const targetFormalName = aliasToNameMap ? aliasToNameMap[targetText] || targetText : targetText;
while (Date.now() - startTime < timeout) {
while (Date.now() - startTime < timeout && retryCount < maxAttempts) {
try {
let resList = captureGameRegion().findMulti(RecognitionObject.ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height));
let captureRegion = captureGameRegion();
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.8;
let resList = captureRegion.findMulti(ocrObject);
for (let res of resList) {
let correctedText = res.text;
for (let [wrongChar, correctChar] of Object.entries(replacementMap)) {
correctedText = correctedText.replace(new RegExp(wrongChar, 'g'), correctChar);
}
if (correctedText.includes(targetText)) {
return { success: true, text: correctedText, x: res.x, y: res.y };
// log.info(`识别结果: ${correctedText}, 原始坐标: x=${res.x}, y=${res.y}`);
let recognizedFormalName = aliasToNameMap ? aliasToNameMap[correctedText] || correctedText : correctedText;
recognizedFormalName = fuzzyMatch(correctedText, Object.values(aliasToNameMap)) || recognizedFormalName;
if (recognizedFormalName === targetFormalName) {
return { success: true, text: recognizedFormalName, x: res.x, y: res.y };
}
}
} catch (error) {
@@ -36,12 +120,75 @@
return { success: false };
}
// 滑动页面函数
// 模糊匹配文本
function fuzzyMatch(target, candidates, weightThreshold = 0.6) {
function levenshteinDistance(a, b) {
const m = a.length + 1;
const n = b.length + 1;
const d = Array(m).fill(null).map(() => Array(n).fill(0));
for (let i = 0; i < m; i++) d[i][0] = i;
for (let j = 0; j < n; j++) d[0][j] = j;
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
}
}
return d[m - 1][n - 1];
}
let bestMatch = null;
let bestWeight = 0;
for (const candidate of candidates) {
const distance = levenshteinDistance(target, candidate);
const keywordWeight = 0.8;
const lengthWeight = 0.2;
const keywordMatch = candidate.includes(target);
const weight = (keywordMatch ? keywordWeight : 0) + (1 - distance / Math.max(target.length, candidate.length)) * lengthWeight;
if (weight >= weightThreshold) {
return candidate;
}
if (weight > bestWeight) {
bestWeight = weight;
bestMatch = candidate;
}
}
return bestMatch;
}
// 合并OCR识别结果
function combineResults(results) {
const frequencyMap = {};
results.forEach(result => {
if (!frequencyMap[result]) {
frequencyMap[result] = 0;
}
frequencyMap[result]++;
});
const sortedResults = Object.keys(frequencyMap).sort((a, b) => frequencyMap[b] - frequencyMap[a]);
for (let result of sortedResults) {
if (result.length === 2) {
return result;
}
}
if (sortedResults.length >= 2) {
return sortedResults[0] + sortedResults[1];
}
return sortedResults[0] || "";
}
// 滚动页面
async function scrollPage(totalDistance, stepDistance = 10, delayMs = 10) {
moveMouseTo(525, 920);
await sleep(500);
leftButtonDown();
const steps = Math.ceil(totalDistance / stepDistance);
for (let j = 0; j < steps; j++) {
const remainingDistance = totalDistance - j * stepDistance;
@@ -49,138 +196,216 @@
moveMouseBy(0, -moveDistance);
await sleep(delayMs);
}
await sleep(700);
leftButtonUp();
await sleep(100);
}
// 角色选择函数
// 选择角色
async function selectCharacter(characterName) {
if (!/^[\u4e00-\u9fa5]+$/.test(characterName)) {
log.error(`Character 设置值无效,必须为纯中文字符。当前值: ${characterName}`);
return false;
}
const SwitchingSteps = 99;
const SwitchingSteps = 99; // 最大切换次数
for (let i = 0; i < SwitchingSteps; i++) {
let result = await recognizeText(characterName, ocrRegion, 100);
let result = await recognizeText(characterName, ocrRegion, aliasToNameMap, 200);
if (result.success) {
log.info(`找到 ${characterName},识别结果: ${result.text},坐标: x=${result.x}, y=${result.y}`);
// log.info(`找到 ${characterName},识别结果: ${result.text},坐标: x=${result.x}, y=${result.y}`);
return true;
}
await click(1840, 540); // 点击切换角色
await sleep(200);
}
log.warn(`扫描完成,未找到 ${characterName}`);
return false;
}
// 元素选择函数
// 选择元素
async function selectElement(element) {
if (element === "物") return; // 如果元素是“物”,不需要切换
const ElementClickX = Math.round(787 + elements.indexOf(element) * 57.5); // 计算点击的X坐标
await click(960, 45); // 移动鼠标到元素选择区域
if (element === "物") return; // 如果是物理属性,无需切换
const ElementClickX = Math.round(787 + elements.indexOf(element) * 57.5); // 计算点击位置
await click(960, 45); // 点击元素切换按钮
await sleep(100);
leftButtonDown(); // 按住鼠标左键
const steps = 10; // 分成若干步移动
const stepDistance = 15; // 每步移动的距离
leftButtonDown();
const steps = 10;
const stepDistance = 15;
for (let j = 0; j < steps; j++) {
moveMouseBy(stepDistance, 0); // 每次移动 stepDistance 像
await sleep(10); // 每次移动后延迟10毫秒
moveMouseBy(stepDistance, 0); // 拖动鼠标选择元
await sleep(10);
}
await sleep(500);
leftButtonUp(); // 释放鼠标左键
leftButtonUp();
await sleep(500);
await click(ElementClickX, 130); // 点击目标元素
await click(ElementClickX, 130); // 点击选择元素
await sleep(500);
// 执行一系列鼠标点击操作
await click(540, 45);
await click(540, 45); // 点击确认
await sleep(200);
}
// 武器扫描函数
async function scanWeapons(weaponName) {
const startX = 99.5;
const startY = 213.5;
const rowHeight = 167;
const columnWidth = 141;
const maxRows = 4;
const maxColumns = 4;
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
for (let row = 0; row < maxRows; row++) {
for (let column = 0; column < maxColumns; column++) {
const clickX = Math.round(startX + column * columnWidth);
const clickY = Math.round(startY + row * rowHeight);
await click(clickX, clickY);
await sleep(50);
let result = await recognizeText(weaponName, ocrRegion, 100);
if (result.success) {
log.info(`找到 ${weaponName},识别结果: ${result.text},坐标: x=${clickX}, y=${clickY}`);
await click(1600, 1005);
await sleep(1000);
await click(1320, 755);
await sleep(1000);
return true;
// 识别并组合武器名称
async function recognizeAndCombineWeaponName(ocrRegion, maxAttempts = 5) {
const allResults = [];
for (let i = 0; i < maxAttempts; i++) {
try {
let captureRegion = captureGameRegion();
let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height);
ocrObject.threshold = 0.8;
let resList = captureRegion.findMulti(ocrObject);
for (let res of resList) {
let correctedText = res.text;
for (let [wrongChar, correctChar] of Object.entries(replacementMap)) {
correctedText = correctedText.replace(new RegExp(wrongChar, 'g'), correctChar);
}
// log.info(`OCR 识别结果: ${correctedText}, 原始坐标: x=${res.x}, y=${res.y}`);
allResults.push(correctedText);
}
} catch (error) {
log.warn(`OCR 识别失败,正在进行第 ${i + 1} 次重试...`);
}
if (scroll < pageScrollCount) {
await scrollPage(673, 10, 10);
}
await sleep(20);
}
const combinedResult = combineResults(allResults);
// log.info(`组合后的识别结果: ${combinedResult}`);
return combinedResult;
}
// 扫描武器
async function scanWeapons(settingsWeapon) {
// 获取角色的正式名称
const characterName = aliasToNameMap[Character] || Character;
log.info(`寻找到角色 ${Character} 正式名称 ${characterName}`);
if (!characterName) {
log.error(`未找到角色 ${Character} 的正式名称`);
return false;
}
// 主流程函数
// 获取角色的武器类型
const characterWeaponType = nameToWeaponMap[characterName];
log.info(`寻找到角色 ${Character} 的武器类型为 ${characterWeaponType}`);
if (!characterWeaponType) {
log.warn(`未找到角色 ${characterName} 的武器类型,将直接匹配 目标武器名 和 当前武器名 `);
}
// 获取对应的武器名称列表
const weaponType = weaponTypeMap[characterWeaponType];
const weaponNames = weaponNamesMap[weaponType] || [];
// 如果武器名称列表为空,则将 settingsWeapon 添加到列表中,以便后续匹配
if (!weaponNames.length) {
log.warn(`未找到武器类型 ${weaponType} 的武器名称列表,将使用原始武器名 ${settingsWeapon}`);
weaponNames.push(settingsWeapon);
}
let weaponName1 = fuzzyMatch(settingsWeapon, weaponNames, 0.9);
log.info(`寻找到 目标武器 正式名称 ${weaponName1}`);
if (!weaponName1) {
log.warn(`未找到与 ${settingsWeapon} 匹配的武器名,使用原始名称作为 目标武器名`);
weaponName1 = settingsWeapon;
}
const startX = 99.5;
const startY = 213.5;
const rowHeight = 167;
const columnWidth = 141;
const maxRows = 4;
const maxColumns = 4;
for (let scroll = 0; scroll <= pageScrollCount; scroll++) {
for (let row = 0; row < maxRows; row++) {
for (let column = 0; column < maxColumns; column++) {
const clickX = Math.round(startX + column * columnWidth);
const clickY = Math.round(startY + row * rowHeight);
await click(clickX, clickY); // 点击武器
await sleep(50);
const combinedWeaponName2 = await recognizeAndCombineWeaponName(ocrRegion);
if (!combinedWeaponName2) {
log.warn("OCR 识别失败,未找到任何武器名");
continue;
}
// 尝试模糊匹配武器名称
let weaponName2 = fuzzyMatch(combinedWeaponName2, weaponNames, 1);
// 如果未匹配到已知武器名称,则将 OCR 识别结果直接作为武器名称使用
if (!weaponName2) {
log.warn(`未找到与 ${combinedWeaponName2} 匹配的已知武器名,将使用 OCR 识别结果作为 当前武器名`);
weaponName2 = combinedWeaponName2;
}
// 计算匹配占比,排除干扰词
const matchRatio = calculateMatchRatio(weaponName1, weaponName2);
if (matchRatio >= 0.8) { // 如果匹配占比大于等于 80%,则认为匹配成功
log.info(`成功匹配武器:${weaponName1},匹配占比 ${matchRatio.toFixed(2)}`);
await click(1600, 1005); // 点击确认
await sleep(1000);
await click(1320, 755); // 点击确认
await sleep(1000);
return true;
} else {
log.warn(` 目标武器名 (${weaponName1}) 和 当前武器名 (${weaponName2}) 不匹配,匹配占比 ${matchRatio.toFixed(2)}`);
}
}
}
if (scroll < pageScrollCount) {
await scrollPage(673, 10, 10); // 滚动页面
}
}
log.warn(`扫描完成,未找到 ${settingsWeapon}`);
return false;
}
// 计算匹配占比
function calculateMatchRatio(target, candidate) {
const ignoreWords = ["剑", "之", "弓", "枪", "长", "大", "典", "章"]; // 需要排除的干扰词
const targetClean = target.split('').filter(char => !ignoreWords.includes(char)).join('');
const candidateClean = candidate.split('').filter(char => !ignoreWords.includes(char)).join('');
const commonChars = targetClean.split('').filter(char => candidateClean.includes(char)).length;
const totalChars = targetClean.length;
return commonChars / totalChars;
}
// 执行角色路径
async function CharacterPath() {
log.info("开始寻找");
await genshin.returnMainUi();
keyPress("1");
await genshin.returnMainUi(); // 返回主界面
keyPress("1"); // 按键操作
await sleep(500);
keyPress("C");
keyPress("C"); // 打开角色界面
await sleep(1000);
await selectElement(Element);
await selectElement(Element); // 选择元素属性
if (!await selectCharacter(Character)) {
if (!await selectCharacter(Character)) { // 选择角色
log.error("角色筛选失败,退出脚本");
return;
}
await click(125, 225); // 点击武器选项
await click(125, 225); // 点击武器详情
await sleep(1000);
await click(1600, 1005); // 点击替换当前武器
await click(1600, 1005); // 点击替换武器
await sleep(1000);
await click(500, 1005); // 使用等级顺序排列
await click(500, 1005); // 点击武器排序
await sleep(200);
await click(500, 905); // 使用等级顺序排列
await click(500, 905); // 点击排序类型
await sleep(200);
await click(605, 137); // 初始化滑条
await moveMouseTo(605, 140);
await sleep(200); // 初始化滑条
leftButtonDown();
await sleep(500);
await click(605, 137); // 点击武器排序
await moveMouseTo(605, 140); // 移动鼠标至滑条顶端
await sleep(200);
leftButtonDown(); // 长按左键重置武器滑条
await sleep(300);
leftButtonUp();
await sleep(200);
if (!await scanWeapons(Weapon)) {
log.warn(`扫描完,未找到 ${Weapon}`);
if (!await scanWeapons(Weapon)) { // 未找到指定武器
log.warn(` ${pageScrollCount+1}扫描完,未找到 ${Weapon}`);
}
await genshin.returnMainUi();
await genshin.returnMainUi(); // 返回主界面
}
// 调用主流程
await CharacterPath();
})();

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 1,
"name": "选择角色和武器 ",
"version": "2.01",
"description": "OCR。默认四行为一页",
"version": "2.22",
"description": "OCR。默认四行为一页v2.1增加模糊匹配增加别名匹配。v2.22兼容未知角色、未知武器",
"authors": [
{
"name": "吉吉喵"

View File

@@ -2,17 +2,17 @@
{
"name": "Character",
"type": "input-text",
"label": "角色名称(默认 纳西妲)"
"label": "角色(如 草神、奶奶)"
},
{
"name": "Weapon",
"type": "input-text",
"label": "武器名称(默认 试作金珀)"
"label": "武器(如 金珀、祭礼)"
},
{
"name": "Element",
"type": "select",
"label": "=============\n\n选元素缩范围 非必填\n元素默认'物'=不选)",
"label": "=============\n选元素缩范围 非必填\n元素默认'物'=不选)",
"options": [
"物",
"火",