(async function () { // 仅适配敌方1-4张牌[我方3张牌]的情况其他情况可能不适配,角色装备检测(根据已有数据)[后续实现],冰冻切人逻辑,全局额外延迟加入设置,敌方骰子数量识别,设置内更改流浪者名称【不支持流浪者】、【注意:双方召唤区与支援区读取已关闭,手牌详情读取已关闭】、死循环内的重投骰子检测要再次确认队伍角色的存活情况、角色倒地仍会切换倒地角色 // 所有图片的路径信息 const pic_path_dic = { "MainPyro": "assets/dice/MainPyro.png", "MainHydro": "assets/dice/MainHydro.png", "MainAnemo": "assets/dice/MainAnemo.png", "MainElectro": "assets/dice/MainElectro.png", "MainDendro": "assets/dice/MainDendro.png", "MainCryo": "assets/dice/MainCryo.png", "MainGeo": "assets/dice/MainGeo.png", "MainOmni": "assets/dice/MainOmni.png", "RollPyro": "assets/dice/RollPyro.png", "RollHydro": "assets/dice/RollHydro.png", "RollAnemo": "assets/dice/RollAnemo.png", "RollElectro": "assets/dice/RollElectro.png", "RollDendro": "assets/dice/RollDendro.png", "RollCryo": "assets/dice/RollCryo.png", "RollGeo": "assets/dice/RollGeo.png", "RollOmni": "assets/dice/RollOmni.png", "Hand5": "assets/num/Hand5.png", "Hand6": "assets/num/Hand6.png", "Hand7": "assets/num/Hand7.png", "Hand8": "assets/num/Hand8.png", "Hand9": "assets/num/Hand9.png", "Hand10": "assets/num/Hand10.png", "disable": "assets/disable.png", "setting": "assets/setting.png", "userTurn": "assets/user_turn.png", "enemyTurn": "assets/enemy_turn.png", "charge": "assets/charge.png", "uncharge": "assets/uncharge.png", "StatePyro": "assets/state/StatePyro.png", "StateHydro": "assets/state/StateHydro.png", "StateElectro": "assets/state/StateElectro.png", "StateDendro": "assets/state/StateDendro.png", "StateCryo": "assets/state/StateCryo.png", "StateFreeze": "assets/state/StateFreeze.png", } // 包含异常状态[State](其他状态从第12项开始,用于显示User.userState和User.enemyState) const element_dic = { "无": "None", "火": "Pyro", "水": "Hydro", "风": "Anemo", "雷": "Electro", "草": "Dendro", "冰": "Cryo", "岩": "Geo", "万能": "Omni", "物理": "Physical", "穿透": "Piercing", "冰冻": "Freeze" } // 定位角色牌充能的位置(识别角色牌充能用,覆盖整个牌的充能区域) const chargeArea_dic = { "size": [32, 180], "enemy31": [812, 185], "enemy32": [1022, 185], "enemy33": [1233, 185], "user31": [812, 612], "user32": [1022, 612], "user33": [1233, 612], "enemy41": [708, 185], "enemy42": [919, 185], "enemy43": [1126, 185], "enemy44": [1337, 185], "enemy21": [918, 185], "enemy22": [1128, 185], "enemy11": [1022, 185] } // 定位角色牌位置(识别异常状态用,覆盖整个牌面及上方部分区域) const stateArea_dic = { "size": [167, 382], "enemy31": [663, 105], "enemy32": [876, 105], "enemy33": [1089, 105], "user31": [663, 545], "user32": [876, 545], "user33": [1089, 545], "enemy41": [562, 105], "enemy42": [774, 105], "enemy43": [984, 105], "enemy44": [1193, 105], "enemy21": [857, 185], "enemy22": [1070, 185], "enemy11": [1022, 185] } // 定位手牌位置(1-10张手牌) const stateHand_dic = { 1: { 1: [1120, 945] }, 2: { 1: [1019, 945], 2: [1222, 945] }, 3: { 1: [914, 945], 2: [1117, 945], 3: [1322 , 945] }, 4: { 1: [812, 945], 2: [1013, 945], 3: [1216, 945], 4: [1424, 945] }, 5: { 1: [719, 945], 2: [913, 945], 3: [1115, 945], 4: [1319, 945], 5: [1528, 945] }, 6: { 1: [614, 945], 2: [811, 945], 3: [1012, 945], 4: [1216, 945], 5: [1423, 945], 6: [1625, 945] }, 7: { 1: [635, 945], 2: [799, 945], 3: [961, 945], 4: [1117, 945], 5: [1279, 945], 6: [1438, 945], 7: [1603, 945] }, 8: { 1: [605, 945], 2: [751, 945], 3: [893, 945], 4: [1042, 945], 5: [1177, 945], 6: [1325, 945], 7: [1471, 945], 8: [1618, 945] }, 9: { 1: [587, 945], 2: [716, 945], 3: [847, 945], 4: [976, 945], 5: [1103, 945], 6: [1225, 945], 7: [1352, 945], 8: [1492, 945], 9: [1634, 945] }, 10: { 1: [563, 945], 2: [686, 945], 3: [892, 945], 4: [920, 945], 5: [1039, 945], 6: [1153, 945], 7: [1270, 945], 8: [1384, 945], 9: [1507, 945], 10: [1649, 945] } } // 角色牌血量坐标(0为x,1、2为两个y值[分别对应上下两个位置]) const hpArea_dic = { "size": [58, 40], "enemy31": [646, 170, 212], "enemy32": [856, 170, 212], "enemy33": [1066, 170, 212], "user31": [646, 600, 640], "user32": [856, 600, 640], "user33": [1066, 600, 640], "enemy41": [541, 170, 212], "enemy42": [751, 170, 212], "enemy43": [961, 170, 212], "enemy44": [1171, 170, 212], "enemy21": [751, 170, 212], "enemy22": [962, 170, 212], "enemy11": [856, 170, 212] } // 角色牌技能位置(0为四技能角色的普攻位置,1为三技能角色的普攻位置) let skillPosition_dic = { 0: [1500, 957], 1: [1608, 957], 2: [1716, 957], 3: [1824, 957] } // 元素反应字典(左侧为附着元素)[不可更改顺序]【DEBUG】还需要改进,这里仅考虑伤害增幅和对后台角色的额外伤害,不考虑激化、绽放、碎冰(目前添加较为简单)等后续伤害增幅和召唤物 let elementReaction_dic = { "Pyro": { "Pyro": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Hydro": { "increase": 2, "others": { "damage": 0, "element_type": "" } }, "Anemo": { "increase": 0, "others": { "damage": 1, "element_type": "Pyro" } }, "Electro": { "increase": 2, "others": { "damage": 0, "element_type": "" } }, "Dendro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Cryo": { "increase": 2, "others": { "damage": 0, "element_type": "" } }, "Geo": { "increase": 1, "others": { "damage": 0, "element_type": "" } } }, "Hydro": { "Pyro": { "increase": 2, "others": { "damage": 0, "element_type": "" } }, "Hydro": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Anemo": { "increase": 0, "others": { "damage": 1, "element_type": "Hydro" } }, "Electro": { "increase": 1, "others": { "damage": 1, "element_type": "Piercing" } }, "Dendro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Cryo": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Geo": { "increase": 1, "others": { "damage": 0, "element_type": "" } } }, "Electro": { "Pyro": { "increase": 2, "others": { "damage": 0, "element_type": "" } }, "Hydro": { "increase": 1, "others": { "damage": 1, "element_type": "Piercing" } }, "Anemo": { "increase": 0, "others": { "damage": 1, "element_type": "Electro" } }, "Electro": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Dendro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Cryo": { "increase": 1, "others": { "damage": 1, "element_type": "Piercing" } }, "Geo": { "increase": 1, "others": { "damage": 0, "element_type": "" } } }, "Cryo": { "Pyro": { "increase": 2, "others": { "damage": 0, "element_type": "" } }, "Hydro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Anemo": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Electro": { "increase": 1, "others": { "damage": 1, "element_type": "Piercing" } }, "Dendro": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Cryo": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Geo": { "increase": 1, "others": { "damage": 0, "element_type": "" } } }, "Dendro": { "Pyro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Hydro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Anemo": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Electro": { "increase": 1, "others": { "damage": 0, "element_type": "" } }, "Dendro": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Cryo": { "increase": 0, "others": { "damage": 0, "element_type": "" } }, "Geo": { "increase": 0, "others": { "damage": 0, "element_type": "" } } }, } // 全部卡牌信息(本地读取) const card_dic = await parseCardData(); /** * 所有卡牌的基类 */ class Card { constructor(name, type) { this.name = name; // 卡牌名称(例如“卡齐娜”、“以极限之名”等) this.type = type; // 卡牌大类(角色牌、附属牌、行动牌、魔物牌) this.triggers = {}; // 【DEBUG】占位,存放诸如“入场时”、“结束阶段”的触发器函数 } } /** * 角色牌(含血量、技能、附属牌子卡等信息) */ class CharacterCard extends Card { constructor({ name, full_name, health, element, charge, weapon, team, means, skills, children }) { super(name, "角色牌"); this.full_name = full_name; this.health = health; this.element = element; this.charge = charge; this.weapon = weapon; this.team = team; this.means = means; this.skills = skills || []; // children 表示角色的附属牌名称数组 this.children = children || []; } } /** * 附属牌(一般是召唤物,具有触发效果,常在“结束阶段”生效) */ class SummonCard extends Card { constructor({ name, ctype, effect_text, cost }) { super(name, "附属牌"); this.ctype = ctype; // 召唤物标识等 this.effect_text = effect_text; // 效果文字描述 this.cost = cost; this.effect = createActionFromEffectText(effect_text); } } /** * 行动牌(事件牌、装备牌、场地牌等) */ class ActionCard extends Card { constructor({ name, ctype, label, means, cost, effect_text }) { super(name, "行动牌"); this.ctype = ctype; this.label = label; this.means = means; this.cost = cost; // 花费(仅记录,不做计算) this.effect_text = effect_text; // 效果文字描述 this.effect = createActionFromEffectText(effect_text); } } /** * 魔物牌 * 注意:没有魔物牌的附属牌信息 */ class MonsterCard extends Card { constructor({ name, health, element, charge, weapon, team, means, skills }) { super(name, "魔物牌"); this.full_name = full_name; this.health = health; this.element = element; this.charge = charge; this.weapon = weapon; this.team = team; this.means = means; this.skills = skills || []; } } class Game { constructor(options) { /** * 字典内的内容(除了enemyCharge、userCharge、enemyState和userState) * { * 0: { // id,用于判断顺序,从0开始 * "name": "card_name", // 卡牌名 * "object": Object // 对应的卡牌类的实例(由createCardFromDict创建) * } * } * 字典内的内容(enemyCharge和userCharge) * { * 0: [ // id,用于判断顺序,从0开始 * 充能数, * 未充能数 * ] * } * 字典内的内容(enemyState和userState) * { * 0: [ // id,用于判断顺序,从0开始 * "Pyro", * "Freeze", * ... * ] * } */ this.enemyMainCard = ""; // 敌方-出战角色牌 this.userMainCard = ""; // 我方-出战角色牌 this.enemyMainCardId = 0; // 敌方-出战角色牌id this.userMainCardId = 0; // 我方-出战角色牌id this.enemyCards = {}; // 敌方-角色牌 this.enemyLocationArea = {}; // 敌方-场地区 this.enemySummonArea = {}; // 敌方-召唤区 this.userCards = {}; // 我方-角色牌 this.userLocationArea = {}; // 我方-场地区 this.userSummonArea = {}; // 我方-召唤区 this.enemyCharge = {}; // 敌方-充能数 this.userCharge = {}; // 我方-充能数 this.enemyState = {}; // 敌方-元素/异常状态 this.userState = {}; // 我方-元素/异常状态 this.enemyHp = {}; // 敌方-角色牌血量 this.userHp = {}; // 我方-角色牌血量 this.effectDic = {}; // 元素反应造成的影响,由game.calculateEachReacctionEffect()生成 } // 获取出战角色的id async calculateId() { for (const [id, msg] of Object.entries(this.enemyCards)) { if (msg["name"] === this.enemyMainCard) { this.enemyMainCardId = id; } } for (const [id, msg] of Object.entries(this.userCards)) { if (msg["name"] === this.userMainCard) { this.userMainCardId = id; } } } // 封装通用逻辑,遍历所有坐标进行点击识别 async getMainCard(cardCoords, extraWaitTime = 0) { const waitTime = 500 + extraWaitTime; for (const { x, y } of cardCoords) { click(x, y); await sleep(waitTime); const cardName = await ocrCardName(); if (cardName !== false) { // 点击屏幕中间(重置状态) click(1190, 545); // 点击屏幕中间靠右复位 await sleep(waitTime); return cardName; } } return false; // 如果所有坐标处理后依然未获取到内容,则返回 false } // 获取敌方出战角色牌 [DEBUG] 应考虑卡牌数变化的情况 async getEnemyMainCard(extraWaitTime = 0) { const enemyCoords = [ { x: 696, y: 460 }, { x: 908, y: 460 }, { x: 1018, y: 460 }, { x: 1231, y: 460 } ]; this.enemyMainCard = await this.getMainCard(enemyCoords, extraWaitTime); } // 获取我方出战角色牌 async getUserMainCard(extraWaitTime = 0) { const myCoords = [ { x: 751, y: 608 }, { x: 962, y: 608 }, { x: 1174, y: 608 } ]; this.userMainCard = await this.getMainCard(myCoords, extraWaitTime); } async getEnemyCard(x, id, waitTime) { // 通用处理函数,用于点击指定位置、等待、OCR获取名称、退出详情界面,并将卡牌数据存入 enemyCards click(x, 330); // 点击指定位置 await sleep(waitTime); // 等待详情界面打开 let cardName; for (let i = 0; i < 3; i++) { // 最多试3次获取卡牌名称 cardName = await ocrCardName(); await sleep(waitTime); if (cardName !== false) break; } await sleep(waitTime); click(1190, 545); // 点击屏幕中间靠右复位 // 退出详情界面 await sleep(waitTime); // 将获取到的卡牌数据存入 enemyCards 中 this.enemyCards[id] = { name: cardName, object: await createCardFromDict(cardName) }; } async getUserCard(x, id, waitTime) { // 通用处理函数,用于点击指定位置、等待、OCR获取名称、退出详情界面,并将卡牌数据存入 userCards click(x, 720); // 点击指定位置 await sleep(waitTime); // 等待详情界面打开 let cardName; for (let i = 0; i < 3; i++) { // 最多试3次获取卡牌名称 cardName = await ocrCardName(); await sleep(waitTime); if (cardName !== false) break; } await sleep(waitTime); click(1190, 545); // 点击屏幕中间靠右复位 // 退出详情界面 await sleep(waitTime); // 将获取到的卡牌数据存入 userCards 中 this.userCards[id] = { name: cardName, object: await createCardFromDict(cardName) }; } // 获取敌方角色牌名称和充能 async getEnemyCards(extraWaitTime = 0) { const waitTime = 200 + extraWaitTime; // 判断敌方角色牌数量 let cardNum; // 首先点击一个位置来判断敌方卡牌的数量(这里用敌方第四张牌的位置做检测) click(1275, 300); await sleep(waitTime); let cardCheck = await ocrCardName(); click(1190, 545); // 点击屏幕中间靠右复位 // 退出卡牌界面 await sleep(waitTime); if (cardCheck !== false) { cardNum = 4; } else { // 尝试点击敌方第三张牌的位置(假设总共三张牌) click(1200, 300); await sleep(waitTime); cardCheck = await ocrCardName(); click(1190, 545); // 点击屏幕中间靠右复位 // 退出卡牌界面 await sleep(waitTime); if (cardCheck !== false) { cardNum = 3; } else { // 尝试点击敌方第二张牌的位置(假设总共两张牌) click(1061, 300); await sleep(waitTime); cardCheck = await ocrCardName(); click(1190, 545); // 点击屏幕中间靠右复位 // 退出卡牌界面 await sleep(waitTime); if (cardCheck !== false) { cardNum = 2; } else { cardNum = 1; } } } await sleep(waitTime); // 等待UI稳定 let gameRegion = captureGameRegion(); // 捕获一次游戏区域 if (cardNum === 1) { // 一张牌 this.enemyCharge[0] = await getCharge("enemy", 1, 1, gameRegion); this.enemyState[0] = await getState("enemy", 1, 1, gameRegion); this.enemyHp[0] = await getCardHp("enemy", 1, 1, gameRegion); await this.getEnemyCard(959, 0, waitTime); } else if (cardNum === 2) { // 两张牌 for (let i = 0; i < 2; i++) { this.enemyCharge[i] = await getCharge("enemy", i + 1, 2, gameRegion); this.enemyState[i] = await getState("enemy", i + 1, 2, gameRegion); this.enemyHp[i] = await getCardHp("enemy", i + 1, 2, gameRegion); } await this.getEnemyCard(857, 0, waitTime); await this.getEnemyCard(1070, 1, waitTime); } else if (cardNum === 3) { // 三张牌 for (let i = 0; i < 3; i++) { this.enemyCharge[i] = await getCharge("enemy", i + 1, 3, gameRegion); this.enemyState[i] = await getState("enemy", i + 1, 3, gameRegion); this.enemyHp[i] = await getCardHp("enemy", i + 1, 3, gameRegion); } await this.getEnemyCard(750, 0, waitTime); await this.getEnemyCard(960, 1, waitTime); await this.getEnemyCard(1175, 2, waitTime); } else if (cardNum === 4) { // 四张牌 for (let i = 0; i < 4; i++) { this.enemyCharge[i] = await getCharge("enemy", i + 1, 4, gameRegion); this.enemyState[i] = await getState("enemy", i + 1, 4, gameRegion); this.enemyHp[i] = await getCardHp("enemy", i + 1, 4, gameRegion); } await this.getEnemyCard(648, 0, waitTime); await this.getEnemyCard(860, 1, waitTime); await this.getEnemyCard(1070, 2, waitTime); await this.getEnemyCard(1275, 3, waitTime); } gameRegion.dispose(); } // 获取我方角色牌名称和充能 async getUserCards(extraWaitTime = 0) { const waitTime = 200 + extraWaitTime; await sleep(waitTime); // 等待UI稳定 let gameRegion = captureGameRegion(); // 捕获一次游戏区域 for (let i = 0; i < 3; i++) { this.userCharge[i] = await getCharge("user", i + 1, 3, gameRegion); this.userState[i] = await getState("user", i + 1, 3, gameRegion); this.userHp[i] = await getCardHp("user", i + 1, 3, gameRegion); } gameRegion.dispose(); await this.getUserCard(750, 0, waitTime); await this.getUserCard(960, 1, waitTime); await this.getUserCard(1175, 2, waitTime); } // 辅助函数:遍历坐标数组,依次点击、等待、调用 OCR 并填充到指定区域 async fillArea(area, coords, extraWaitTime = 0) { const waitTime = 100 + extraWaitTime; let i = 0; for (const { x, y } of coords) { // 点击屏幕中间以重置状态 await sleep(waitTime); click(1190, 545); // 点击屏幕中间靠右复位 await sleep(waitTime + 150); // 额外等待时间,防止卡牌遮挡 click(x, y); await sleep(waitTime); const cardName = await ocrCardName(); if (cardName !== false) { area[i] = { name: cardName, object: await createCardFromDict(cardName), }; i++; } } } // 获取敌方支援牌(和状态) async getEnemyLocationArea(extraWaitTime = 0) { const enemyCoords = [ { x: 321, y: 218 }, { x: 490, y: 218 }, { x: 321, y: 413 }, { x: 490, y: 413 }, ]; await this.fillArea(this.enemyLocationArea, enemyCoords, extraWaitTime); } // 获取我方支援牌(和状态) async getUserLocationArea(extraWaitTime = 0) { const userCoords = [ { x: 321, y: 671 }, { x: 490, y: 671 }, { x: 321, y: 852 }, { x: 487, y: 852 }, ]; await this.fillArea(this.userLocationArea, userCoords, extraWaitTime); } // 获取敌方召唤牌(和状态) async getEnemySummonArea(extraWaitTime = 0) { const enemySummonCoords = [ { x: 1433, y: 218 }, { x: 1596, y: 218 }, { x: 1433, y: 413 }, { x: 1596, y: 413 }, ]; await this.fillArea(this.enemySummonArea, enemySummonCoords, extraWaitTime); } // 获取我方召唤牌(和状态) async getUserSummonArea(extraWaitTime = 0) { const userSummonCoords = [ { x: 1433, y: 671 }, { x: 1596, y: 671 }, { x: 1433, y: 852 }, { x: 1596, y: 852 }, ]; await this.fillArea(this.userSummonArea, userSummonCoords, extraWaitTime); } // 聚合获取当前局面的方法 async get_state(extraWaitTime = 0) { await this.getEnemyMainCard(extraWaitTime); // 敌方出战卡牌 await this.getUserMainCard(extraWaitTime); // 我方出战卡牌 await this.getUserCards(extraWaitTime); // 我方卡牌 await this.getEnemyCards(extraWaitTime); // 敌方卡牌 // await this.getUserLocationArea(extraWaitTime); // 我方支援牌 // await this.getEnemyLocationArea(extraWaitTime); // 敌方支援牌 // // await this.getUserSummonArea(extraWaitTime); // 我方召唤牌 // // await this.getEnemySummonArea(extraWaitTime); // 敌方召唤牌 await this.calculateId(); // 双方出战角色id return true; } // 仅获取双方角色牌信息 async get_character_cards(extraWaitTime = 0) { await this.getEnemyMainCard(extraWaitTime); // 敌方出战卡牌 await this.getUserMainCard(extraWaitTime); // 我方出战卡牌 await this.getUserCards(extraWaitTime); // 我方卡牌 await this.getEnemyCards(extraWaitTime); // 敌方卡牌 await this.calculateId(); // 双方出战角色id } // 计算单独阵营所有角色受到敌方元素攻击造成的元素反应影响【DEBUG】仅参考总体伤害量,暂不考虑造成的元素附着影响 // 【DEBUG】一个角色同时附有冰草附着的情况下,理应不会在一次行动中清除这两种元素 async calculateEachReactionEffect(target) { let enemyElement = ""; this.effectDic = {}; if (target === "enemy") { let elementText = this.userCards[this.userMainCardId]["object"].element; for (const [cn_name, en_name] of Object.entries(element_dic)) { if (elementText.includes(cn_name)) { enemyElement = en_name; break; } } for (const [id, debuffs] of Object.entries(this.enemyState)) { let measure = 0; if (debuffs.length !== 0) { // 有元素附着 let reactionDic = elementReaction_dic[debuffs[0]][enemyElement]; measure += reactionDic["increase"]; if (reactionDic["others"]["damage"] !== 0 && reactionDic["others"]["element_type"] === "Piercing") { // 穿透伤害 measure += Object.keys(this.enemyCards).length - 1; // 其他角色全部受到1点穿透伤害 } else if (enemyElement === "Anemo" && reactionDic["others"]["element_type"] !== "Piercing") { // 扩散伤害 for (const [b_id, b_debuffs] of Object.entries(this.enemyState)) { // 遍历一遍后台角色 if (b_id === id) continue; if (b_debuffs.length !== 0) { // 有元素附着 let b_reactionDic = elementReaction_dic[b_debuffs[0]][reactionDic["others"]["element_type"]]; // 受到出战角色的扩散元素造成的元素反应伤害 measure += b_reactionDic["increase"]; if (reactionDic["others"]["damage"] !== 0 && reactionDic["others"]["element_type"] === "Piercing") { // 穿透伤害 measure += Object.keys(this.enemyCards).length - 1; // 其他角色全部受到1点穿透伤害 } } else { // 无元素附着 measure += 1; // 受到一点元素伤害 } } } } else { // 无元素附着 measure += 1; // 受到一点元素伤害 } this.effectDic[id] = measure; } } else if (target === "user") { let elementText = this.enemyCards[this.enemyMainCardId]["object"].element; // [DEBUG] 此处获取的enemyMainCardId极大概率是上回合的,应在getEnemyMainCardId方法处更正策略 for (const [cn_name, en_name] of Object.entries(element_dic)) { if (elementText.includes(cn_name)) { enemyElement = en_name; break; } } for (const [id, debuffs] of Object.entries(this.userState)) { let measure = 0; if (debuffs.length !== 0) { // 有元素附着 let reactionDic = elementReaction_dic[debuffs[0]][enemyElement]; log.info(`[DEBUG] 出战角色受击预测: ${target}(${id}): ${debuffs[0]}(附着)|${enemyElement}(受击)`); measure += reactionDic["increase"]; if (reactionDic["others"]["damage"] !== 0 && reactionDic["others"]["element_type"] === "Piercing") { // 穿透伤害 measure += Object.keys(this.userCards).length - 1; // 其他角色全部受到1点穿透伤害 } else if (enemyElement === "Anemo" && reactionDic["others"]["element_type"] !== "Piercing") { // 扩散伤害 for (const [b_id, b_debuffs] of Object.entries(this.userState)) { // 遍历一遍后台角色 if (b_id === id) continue; if (b_debuffs.length !== 0) { // 有元素附着 let b_reactionDic = elementReaction_dic[b_debuffs[0]][reactionDic["others"]["element_type"]]; // 受到出战角色的扩散元素造成的元素反应伤害 log.info(`[DEBUG] 出战角色受击预测: ${target}(${id}): ${b_debuffs[0]}(附着)|${reactionDic["others"]["element_type"]}(受击)`); measure += b_reactionDic["increase"]; if (reactionDic["others"]["damage"] !== 0 && reactionDic["others"]["element_type"] === "Piercing") { // 穿透伤害 measure += Object.keys(this.userCards).length - 1; // 其他角色全部受到1点穿透伤害 } } else { // 无元素附着 measure += 1; // 受到一点元素伤害 } } } } else { // 有元素附着 measure += 1; // 受到一点元素伤害 } this.effectDic[id] = measure; } } } // 输出当前牌桌详情 async log_info() { let user_cards = "|"; let enemy_cards = "|"; if (Object.keys(this.userCards).length !== 0) { // log.info(`我方卡牌数量: ${Object.keys(this.userCards).length}`); for (let i = 0; i < Object.keys(this.userCards).length; i++) { let stateText = "-"; for (const [cn_name, en_name] of Object.entries(element_dic)) { for (let j = 0; j < Object.entries(this.userState[i]).length; j++) { if (Object.entries(this.userState[i])[j].includes(en_name)) { stateText += `${cn_name}-`; } } } user_cards += `${this.userCards[Object.keys(this.userCards)[i]]["name"]}${this.userMainCard === this.userCards[Object.keys(this.userCards)[i]]["name"] ? "[出战]": ""}(${this.userCharge[i][0]}/${this.userCharge[i][0] + this.userCharge[i][1]})${stateText !== "-" ? `{${stateText}}`: ""}\<${this.userHp[i]}\>|`; } log.info(`我方卡牌(${Object.keys(this.userCards).length}): ${user_cards}`); } if (Object.keys(this.enemyCards).length !== 0) { for (let i = 0; i < Object.keys(this.enemyCards).length; i++) { let stateText = "-"; for (const [cn_name, en_name] of Object.entries(element_dic)) { for (let j = 0; j < Object.entries(this.enemyState[i]).length; j++) { if (Object.entries(this.enemyState[i])[j].includes(en_name)) { stateText += `${cn_name}-`; } } } enemy_cards += `${this.enemyCards[Object.keys(this.enemyCards)[i]]["name"]}${this.enemyMainCard === this.enemyCards[Object.keys(this.enemyCards)[i]]["name"] ? "[出战]": ""}(${this.enemyCharge[i][0]}/${this.enemyCharge[i][0] + this.enemyCharge[i][1]})${stateText !== "-" ? `{${stateText}}`: ""}\<${this.enemyHp[i]}\>|`; } log.info(`敌方卡牌(${Object.keys(this.enemyCards).length}): ${enemy_cards}`); } let userLocationAreaText = "我方支援牌:|"; if (Object.keys(this.userLocationArea).length !== 0) { for (let i = 0; i < Object.keys(this.userLocationArea).length; i++) { userLocationAreaText += `${this.userLocationArea[i]["name"]}(${i + 1})|`; } log.info(`${userLocationAreaText}`); } let enemyLocationAreaText = "敌方支援牌:|"; if (Object.keys(this.enemyLocationArea).length !== 0) { for (let i = 0; i < Object.keys(this.enemyLocationArea).length; i++) { enemyLocationAreaText += `${this.enemyLocationArea[i]["name"]}(${i + 1})|`; } log.info(`${enemyLocationAreaText}`); } let userSummonAreaText = "我方召唤牌:|"; if (Object.keys(this.userSummonArea).length !== 0) { for (let i = 0; i < Object.keys(this.userSummonArea).length; i++) { userSummonAreaText += `${this.userSummonArea[i]["name"]}(${i + 1})|`; } log.info(`${userSummonAreaText}`); } let enemySummonAreaText = "敌方召唤牌:|"; if (Object.keys(this.enemySummonArea).length !== 0) { for (let i = 0; i < Object.keys(this.enemySummonArea).length; i++) { enemySummonAreaText += `${this.enemySummonArea[i]["name"]}(${i + 1})|`; } log.info(`${enemySummonAreaText}`); } } } class Action { /** * * 根据effect_text解析单个卡牌效果(仅影响出牌决策,具体效果不进行推算[对局数据使用其他方法实时获取]) * * @param {boolean} dealDamage 是否造成伤害 * @param {boolean} instantDamage 是否是即时伤害 * @param {boolean} clickTwice 是否需要二次点击 * 例如指定敌方的召唤牌 * @param {bigint} damage 伤害 * 正数为攻击、负数为治疗 * @param {bigint} residueDegree 剩余次数 * @param {bigint} consumption 生效后减少的可用次数 * @param {string} effectType 生效类型 * 我方出战角色为...(角色被动)user_main_card[卡牌名] * 我方...卡牌入场(名称、类型)user_card_income[卡牌名] * 敌方...卡牌入场(名称、类型)enemy_card_income[卡牌名] * 我方切换角色 user_switch * 敌方切换角色 enemy_switch * 我方造成元素攻击 user_element_dmg * 敌方造成元素攻击 enemy_element_dmg * 我方造成穿透伤害 user_real_dmg * 敌方造成穿透伤害 enemy_real_dmg * 回合结束 round_over * @param {string} elementType 元素类型(元素附着) * 无 None * 火 Pyro * 水 Hydro * 风 Anemo * 雷 Electro * 草 Dendro * 冰 Cryo * 岩 Geo * @param {string} targetType 目标 * 敌方出战卡牌 enemy_main_card * 我方出战卡牌 user_main_card * 敌方下一个角色卡牌 enemy_next_card * 我方下一个角色卡牌 user_next_card * 敌方所有后台卡牌 enemy_other_card * 我方所有后台卡牌 user_other_card * 敌方支援牌 enemy_location * 敌方召唤牌 enemy_summon * 我方支援牌 user_location * 我方召唤牌 user_summon * @param {string} specialEffect 特殊效果 * 例如本回合禁止对方使用行动牌(秘传卡牌等) */ constructor (dealDamage, instantDamage, clickTwice, damage, residueDegree, consumption, effectType, elementType, targetType, specialEffect) { this.dealDamage = dealDamage; // 是否造成伤害(boolean) this.instantDamage = instantDamage; // 是否是即时伤害(boolean) this.clickTwice = clickTwice; // 是否需要二次点击(boolean) this.damage = damage; // 伤害(int) this.residueDegree = residueDegree; // 剩余次数(int) this.consumption = consumption; // 生效后减少的可用次数(int) this.effectType = effectType; // 生效类型(String) this.elementType = elementType; // 元素类型[元素附着](String) this.targetType = targetType; // 目标(String) this.specialEffect = specialEffect; // 特殊效果(String) } } class Strategy { constructor () { this.diceMsg = {}; this.keepDiceList = ["Omni"]; this.TcgGame = new Game(); } /** * 选择手牌开始【DEBUG】需要加入手牌选择功能 */ async waitStartingHand() { if (await isUiStartingHand(false)) { log.info("当前为手牌选择界面"); await sleep(500); click(960, 948); await sleep(200); click(960, 948); // 防止点不上【DEBUG】可以在后续流程加检测 } } /** * 选择出战角色界面 */ async waitChoice() { if (await isUiChoice(false)) { // 识别我方角色牌 await sleep(1500); // 等待界面动画结束 await this.TcgGame.getUserCards(); await this.TcgGame.log_info(); // 输出我方角色牌信息 // 选择出战角色(第一个牌)【DEBUG】出战角色选择逻辑有待完善[根据数据总结] await sleep(200); click(750, 720); await sleep(500); click(1826, 960); await sleep(200); click(1826, 960); // 【DEBUG】此处可以添加动态检测逻辑 } } /** * 重投界面 */ async waitReroll(extraWaitTime = 0) { let waitTime = 1000 + extraWaitTime; // 重投骰子 if (await isUiReroll(false)) { // 尚未读取我方角色牌信息,优先读取(此处读取双方角色牌信息) if (Object.keys(this.TcgGame.userCards).length === 0) { click(1872, 49); // 点击右上角按钮查看牌桌 await sleep(waitTime); await this.TcgGame.get_character_cards(); await this.TcgGame.log_info(); await sleep(waitTime); click(1872, 49); // 点击右上角按钮返回重投界面 await sleep(waitTime); } if (this.keepDiceList.length === 1) { // 检测骰子信息并根据角色牌保留骰子【DEBUG】保留骰子的逻辑应根据牌组适配 for (let i = 0; i < Object.keys(this.TcgGame.userCards).length; i++) { for (const [char, name] of Object.entries(element_dic)) { if (card_dic[this.TcgGame.userCards[i]["name"]]["element"].includes(char) && !(this.keepDiceList.includes(name))) { this.keepDiceList.push(name); } } } } this.diceMsg = await getDiceMsg(this.keepDiceList); await sleep(waitTime); click(968, 950); await sleep(waitTime); click(968, 950); // 【DEBUG】此处可以添加动态检测逻辑 } } async strategyZero() { while (true) { // 主要循环 // 选择手牌开始【DEBUG】需要加入手牌选择功能 await this.waitStartingHand(); // 选择出战角色界面 await this.waitChoice(); // 检测对局结束【DEBUG】结束条件检测(成功或者失败添加不同条件) if (await isUiFinish(false) !== "none") break; // 重投骰子 await this.waitReroll(); // 识别当前回合归属 let turnStatus = await getActingTurn(false); // 回合动作 if (turnStatus === "user") { // 我方回合 await sleep(100); // test // 本回合行动标志 let continueFlag = true; // 获取牌桌信息 await this.TcgGame.get_state(); await this.TcgGame.log_info(); // 获取骰子信息 this.diceMsg = await getDiceMsg(); let diceNum = Object.values(this.diceMsg).reduce((acc, current) => acc + current, 0); // 检测元素骰是否充足 【DEBUG】此处也要考虑敌方骰子数量 if (diceNum <= 1) { log.info("元素骰子不足,结束回合"); // 结束回合 await terminateTurn(); continutFlag = false; } // 获取手牌信息【DEBUG】暂时无用,手牌部分仅手牌数量有用 // // 【DEBUG】目前暂时为烧牌,后续改进通过结合Action类进行牌型分析后打出有效手牌 // let handMsg = await getHandMsg(await getHandNum()); // 我方回合行动策略【主要部分】------------------------------------------------《>>|*|<<》 // 此处敌方出战角色的元素附着状态,血量和我方出战角色血量已知 // 【开局策略】【附着元素,防止自身被附着元素】 // 【猛攻策略】【释放元素爆发】检测充能和敌方附着 // 【策略1】【防御:应对敌方充能满】 if (this.TcgGame.enemyCharge[this.TcgGame.enemyMainCardId][1] === 0 && continueFlag) { // 敌方出战卡牌的未充能数为0【满充能】 log.info("敌方出战角色牌充能已满,通过切人应对"); if (this.TcgGame.userHp[this.TcgGame.userMainCardId] <= 3) { // 血量过低 // 切人 // 计算元素影响 let switchList = []; await this.TcgGame.calculateEachReactionEffect("user"); for (const [id, measure] of Object.entries(this.TcgGame.effectDic)) { switchList.push(this.TcgGame.userHp[id] - measure); } // 获取最大值 const maxValue = Math.max(...switchList); // 找到最大值的索引 const maxIndex = switchList.indexOf(maxValue); // 如果最优角色牌不是出战角色就切人 if (this.TcgGame.userMainCardId !== maxIndex) { await switchCard(maxIndex); continueFlag = false; } } else { // 血量充足,检测元素反应 // 计算元素影响 await this.TcgGame.calculateEachReactionEffect("user"); let switch_flag = false; for (const [name, msg] of Object.entries(elementReaction_dic)) { if (switch_flag) break; if (this.TcgGame.userState[this.TcgGame.userMainCardId].includes(name)) { // 被附着了可以发生元素反应的元素 for (const [cn_name, en_name] of Object.entries(element_dic)) { if (this.TcgGame.enemyCards[this.TcgGame.enemyMainCardId]["object"].element.includes(cn_name)) { // 找到敌方出战卡牌元素的英文字符串 if (Object.keys(msg).includes(en_name)) { // 如果可以发生元素反应 // 切人 // 计算最优出战角色 // 获取最小值 const minValue = Math.min(...Object.values(this.TcgGame.effectDic)); // 找到最小值的索引 const minIndex = Object.values(this.TcgGame.effectDic).indexOf(minValue); // 如果最优角色不是出战角色 if (this.userMainCardId !== minIndex) { await switchCard(minIndex); } switch_flag = true; continueFlag = false; break; } } } } } } // 【策略2】【利用敌方已有附着触发反应】 // 敌方存在元素附着并且元素骰子大于等于3【DEBUG】骰子不一定要大于等于3,需要加一个实时判断,也需要一个手牌数判断 } if (Object.keys(this.TcgGame.enemyState[this.TcgGame.enemyMainCardId]).length !== 0 && diceNum >= 3 && continueFlag) { log.info("敌方出战角色具有元素附着,利用敌方已有附着触发反应"); let enemyElement = ""; let userElement = ""; if (this.TcgGame.enemyState[this.TcgGame.enemyMainCardId].length !== 0 ) enemyElement = this.TcgGame.enemyState[this.TcgGame.enemyMainCardId][0]; for (const [cn_name, en_name] of Object.entries(element_dic)) { if (this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].element.includes(cn_name)) { userElement = en_name; break; } } log.info(`${enemyElement}|${userElement}`); if (enemyElement && Object.keys(elementReaction_dic[enemyElement]).includes(userElement)) { // 可以触发元素反应 // 我方角色释放技能(元素调和) // // 1.获取骰子信息 // let this.diceMsg = await getDiceMsg(); // 1.选择具备元素附着的技能 let skillId = 1; let skillList = this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].skills; for (let i = 0; i < skillList.length; i++) { let currentAction = await createActionFromEffectText(skillList[i]["effect_text"]); if (Object.keys(element_dic).slice(1, 8).includes(currentAction.elementType)) { skillId = i; break; } } // 2.计算技能花费 let needCostDic = await calculateSkillCost(skillId, this.TcgGame, this.diceMsg); if (needCostDic["isEnough"]) { // 释放技能 await useSkill(skillId, this.TcgGame.userMainCard); } else if (!needCostDic["isEnough"] && needCostDic["needNum"] !== -1) { // 骰子不满足释放要求,可通过元素调和释放 let currentHandNum = await getHandNum(); if (currentHandNum >= needCostDic["needNum"]) { // 手牌足够用来烧牌 await elementalTuning(needCostDic["needNum"]); await useSkill(skillId, this.TcgGame.userMainCard); continueFlag = false; } } else if (!needCostDic["isEnough"] && needCostDic["needNum"] === -1) { // 骰子不足无法释放 await sleep(200); } } // 【策略3】【主动给敌方附上元素,需要至少有两个元素骰】 } if (this.TcgGame.enemyState[this.TcgGame.enemyMainCardId].length === 0 && diceNum > 2 && continueFlag) { log.info("敌方出战卡牌未附有元素,主动给敌方附上元素") // 选择具备元素附着的技能 let skillId = 0; let skillList = this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].skills; for (let i = 0; i < skillList.length; i++) { let currentAction = await createActionFromEffectText(skillList[i]["effect_text"]); if (currentAction.elementType.includes(Object.keys(element_dic).slice(1, 8))) { skillId = i; break; } } // 我方角色释放技能(元素调和) let needCostDic = await calculateSkillCost(skillId, this.TcgGame, this.diceMsg); if (needCostDic["isEnough"]) { // 释放技能 await useSkill(skillId, this.TcgGame.userMainCard); } else if (!needCostDic["isEnough"] && needCostDic["needNum"] !== -1) { // 骰子不满足释放要求,可通过元素调和释放 let currentHandNum = await getHandNum(); if (currentHandNum >= needCostDic["needNum"]) { // 手牌足够用来烧牌 await elementalTuning(needCostDic["needNum"]); await useSkill(skillId, this.TcgGame.userMainCard); } } else if (!needCostDic["isEnough"] && needCostDic["needNum"] === -1) { // 骰子不足无法释放【DEBUG】这里加了切人逻辑 // 计算元素影响 let switchList = []; await this.TcgGame.calculateEachReactionEffect("user"); for (const [id, measure] of Object.entries(this.TcgGame.effectDic)) { switchList.push(this.TcgGame.userHp[id] - measure); } // 获取最大值 const maxValue = Math.max(...switchList); // 找到最大值的索引 const maxIndex = switchList.indexOf(maxValue); // 如果最优角色牌不是出战角色就切人 if (this.userMainCardId !== maxIndex) { await switchCard(maxIndex); } else { // 如果是出战角色就结束回合 await terminateTurn(); } } } else { // 【DEBUG】有待检验,切换条件不应为只是检测敌方而应该是我方 log.warn("执行备用策略"); if (diceNum < 3 && diceNum > 0) { // 测试用,可用性待验证【DEBUG】此处的可能情境为元素骰子不足 log.info(`${diceNum}`); // 如果敌方出战角色牌血量不是最低的,换我方血量最高的【DEBUG】优先换我方元素附着为对方出战角色元素的高血量牌 if (this.TcgGame.enemyHp[this.TcgGame.enemyMainCardId] > Math.min(...Object.values(this.TcgGame.enemyHp))) { log.info("敌方出战角色牌血量较高,切换我方血量最高的角色牌出战"); for (const [id, hp] of Object.entries(this.TcgGame.userHp)) { if (hp === Math.max(...Object.values(this.TcgGame.userHp))) { if (this.TcgGame.userMainCardId === id) { // 如果要切换的角色为出战角色 break; } else { await switchCard(id); continueFlag = false; break; } } } // 如果我方出战角色牌血量最低,换下一位角色牌为出战角色【DEBUG】,最好是换血量第二低的角色牌,且我方骰子数需要至少为1(这点应该满足) } else if (this.TcgGame.userHp[this.TcgGame.userMainCardId] === Math.min(...Object.values(this.TcgGame.userHp))) { log.info("我方出战角色牌血量最低,切换下一个角色牌出战"); await switchCard((this.TcgGame.userMainCardId + 1) % 3); continueFlag = false; } } await sleep(1000); } // 【策略4】【备选输出[条件可以是元素相同的时候释放或者元素吊着数量大于2时释放]】普通攻击 await sleep(5000); } else if (turnStatus === "enemy") { // 敌方回合 await sleep(500); } else { click(960, 540); await sleep(500); } } } async strategyOne() { while (true) { // 主要循环 // 选择手牌开始【DEBUG】需要加入手牌选择功能 await this.waitStartingHand(); // 选择出战角色界面 await this.waitChoice(); // 检测对局结束【DEBUG】结束条件检测(成功或者失败添加不同条件) if (await isUiFinish(false) !== "none") break; // 重投骰子 await this.waitReroll(); // 识别当前回合归属 let turnStatus = await getActingTurn(false); // 回合动作 if (turnStatus === "user") { // 我方回合 // 获取牌桌信息 await this.TcgGame.get_state(); await this.TcgGame.log_info(); // 获取骰子信息 this.diceMsg = await getDiceMsg(); let diceNum = Object.values(this.diceMsg).reduce((acc, current) => acc + current, 0); // 获取手牌数量 let currentHandNum = await getHandNum(); // 计算元素影响 this.TcgGame.calculateEachReactionEffect("user"); // 计算元素影响后的我方角色牌剩余血量 let restHpDic = {}; log.info(`[DEBUG] 我方当前血量 ${Object.keys(this.TcgGame.userHp)} | ${Object.values(this.TcgGame.userHp)}`); for (const [id, num] of Object.entries(this.TcgGame.userHp)) { restHpDic[id] = num - this.TcgGame.effectDic[id]; } log.info(`[DEBUG] 我方元素影响后剩余血量预测 ${Object.keys(restHpDic)} | ${Object.values(restHpDic)}`); // 声明技能消耗字典,以免重复声明 let costDic; if (diceNum <= 1) { // 我方骰子数小于等于1 结束回合 await terminateTurn(); log.info(`骰子数(${diceNum})小于等于1,结束回合`); } else if (this.TcgGame.userCharge[this.TcgGame.userMainCardId][1] === 0){ // 我方出战角色充能已满 log.info(`我方出战角色(${this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name})充能已满(${this.TcgGame.userCharge[this.TcgGame.userMainCardId][0]}/${this.TcgGame.userCharge[this.TcgGame.userMainCardId][0]+ this.TcgGame.userCharge[this.TcgGame.userMainCardId][1]})`); let skillList = this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].skills; let skillId; for (let i = 0; i < skillList.length; i++) { if (skillList[i]["type"] === "元素爆发") { skillId = skillList[i]["id"]; break; } } let costDic = await calculateSkillCost(skillId, this.TcgGame, this.diceMsg); log.info(`[DEBUG] 元素爆发需求字典 ${costDic["isEnough"]} | ${costDic["needNum"]}`); if (costDic["isEnough"]) { // 满足元素爆发释放条件 await useSkill(skillId, this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name); } else if (costDic["needNum"] === -1) { // 不满足元素爆发释放条件,且无法通过元素调和的方式释放元素爆发 // 切换我方受对方元素影响最低的角色(如果切换的是当前角色,释放普攻) 【DEBUG】应当灵活检测,根据需要选择释放元素战技还是普攻 // 1.切换到指定角色牌 let switchId = Object.values(restHpDic).indexOf(Math.max(...Object.values(restHpDic))); // 2.根据切牌条件细化选择 if (switchId !== this.TcgGame.userMainCardId && this.TcgGame.userHp[switchId] >= 0) { // 若要切换的角色不为当前出战角色且要切换的角色牌血量大于等于0 await switchCard(switchId); } else { // 当前出战角色释放普攻 // 计算普攻的消耗 costDic = await calculateSkillCost(0, this.TcgGame, this.diceMsg); if (costDic["isEnough"]) { // 如果能释放普攻就释放普攻 await useSkill(0, this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name); } else if (costDic["needNum"] === -1) { // 不满足普攻释放条件,且无法通过元素调和的方式释放普攻 // 结束当前回合【DEBUG】很极端的一种情况,尚待测试 await terminateTurn(); } else { // 元素调和后释放普攻 await elementalTuning(costDic["needNum"]); await useSkill(0, this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name); } } } else { // 通过元素调和释放元素爆发 await elementalTuning(costDic["needNum"]); await useSkill(skillId, this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name); } } else if (restHpDic[this.TcgGame.userMainCardId] > 0) { // 我方出战角色经元素反应检测后剩余血量大于0 if (this.TcgGame.enemyState[this.TcgGame.enemyMainCardId].length !== 0) { // 敌方出战角色附有元素 let userElement; for (const [cn_name, en_name] of Object.entries(element_dic)) { if (this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].element.includes(cn_name)) { userElement = en_name; break; } } let currentReactionDic = elementReaction_dic[this.TcgGame.enemyState[this.TcgGame.enemyMainCardId][0]][userElement]; let skills = this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].skills if (currentReactionDic["increase"] !== 0 || currentReactionDic["others"]["damage"] !== 0) { // 我方出战角色元素可以与敌方出战角色的附着元素发生反应 // 释放元素战技 for (let i = 0; i acc + current, 0); log.info("此为模板策略,不可使用,仅供CV,使用时删除此行,回合动作从此行开始"); } else if (turnStatus === "enemy") { // 敌方回合 await sleep(500); } else { click(960, 540); await sleep(500); } } } } /** * 供 findClosestMatch 调用 */ function levenshteinDistance(a, b) { const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, // 替换 matrix[i][j - 1] + 1, // 插入 matrix[i - 1][j] + 1 // 删除 ); } } } return matrix[b.length][a.length]; } /** * * 查找最相似的字符串(用于查找角色牌,最大限度避免OCR偏差导致的异常) * * @param target 目标字符串 * @param candidates 字符串数组 * @returns {null} */ function findClosestMatch(target, candidates) { let closest = null; let minDistance = Infinity; for (const candidate of candidates) { const distance = levenshteinDistance(target, candidate); if (distance < minDistance) { minDistance = distance; closest = candidate; } } return closest; } /** * * 根据卡牌名返回实例化的卡牌类 * * @param card_name 卡牌名称 * @returns {Promise} 实例化的卡牌类 */ async function createCardFromDict(card_name) { // 【DEBUG】新逻辑,仍需检验,使用 Levenshtein 距离衡量两个字符串,选择最相似的角色牌名称[但是在Game的log_info()显示的仍为OCR内容,ocr识别错误的情况下日志会输出错误的卡牌名称] card_name = findClosestMatch(card_name, Object.keys(card_dic)); if (Object.keys(card_dic).includes(card_name)) { // 存在该卡牌 if (card_dic[card_name]["type"] === "角色牌") { return new CharacterCard({ name: card_name, full_name: card_dic[card_name]["full_name"], health: card_dic[card_name]["health"], element: card_dic[card_name]["element"], charge: card_dic[card_name]["charge"], weapon: card_dic[card_name]["weapon"], team: card_dic[card_name]["team"], means: card_dic[card_name]["means"], skills: card_dic[card_name]["skills"], children: card_dic[card_name]["children"] }); } else if (card_dic[card_name]["type"] === "附属牌") { // ctype为召唤物cost为空,ctype为特技有cost return new SummonCard({ name: card_name, ctype: card_dic[card_name]["type"], effect_text: card_dic[card_name]["effect_text"], cost: card_dic[card_name]["cost"] }); } else if (card_dic[card_name]["type"] === "行动牌") { return new ActionCard({ name: card_name, ctype: card_dic[card_name]["ctype"], label: card_dic[card_name]["label"], means: card_dic[card_name]["means"], cost: card_dic[card_name]["cost"], effect_text: card_dic[card_name]["effect_text"] }); } else if (card_dic[card_name]["type"] === "魔物牌") { return new CharacterCard({ name: card_name, health: card_dic[card_name]["health"], element: card_dic[card_name]["element"], charge: card_dic[card_name]["charge"], weapon: card_dic[card_name]["weapon"], team: card_dic[card_name]["team"], means: card_dic[card_name]["means"], skills: card_dic[card_name]["skills"] }); } } else { // 不存在该卡牌 return new Card(card_name, "无"); } } /** * * 从卡牌的effect_text解析出相关的Action * * @param {string} effectText 卡牌的技能描述文本effect_text * @returns {Promise} */ function createActionFromEffectText(effectText) { let dealDamage, instantDamage, clickTwice, damage, residueDegree, consumption, effectType, elementType, targetType, specialEffect; // const regexFull = /造成(\d+)点([\u4e00-\u9fa5]+)伤害/; let healFlag = false; // 标记攻击类型为治疗 const regexFull = /(?<=造成)[\s\S]*?(?=伤害)/; const regexHeal = /(?<=治疗)[^点]*?点/; const regexDamage = /(\d+)(?=点)/; let match = effectText.match(regexFull); let matchHeal = effectText.match(regexHeal); if (!match && matchHeal) { healFlag = true; match = matchHeal; } // dealDamage 是否造成伤害 if (match) dealDamage = true; // instantDamage 是否是即时伤害 【DEBUG】逻辑有待完善 if (effectText.includes("回合开始时:") || effectText.includes("结束阶段:")) { instantDamage = false; } else { instantDamage = true; } // clickTwice 是否需要二次点击 【DEBUG】难以实现,需要结合读取选择的卡牌类型,现阶段解决方法为融牌[未实现] clickTwice = false; // damage 伤害 if (match && match[0].includes("点")) { damage = healFlag ? -1 * parseInt(match[0].match(regexDamage)[0], 10): parseInt(match[0].match(regexDamage)[0], 10); } else { damage = 0; } // residueDegree 剩余次数 【DEBUG】性价比不高且较难实现,暂时搁置 residueDegree = 0; // consumption 生效后减少的可用次数 【DEBUG】性价比不高且较难实现,暂时搁置 consumption = 0; // effectType 生效类型 【DEBUG】测试功能,优先实现 effectType = ""; // elementType 元素类型(元素附着) 【DEBUG】仅判断造成伤害时的元素附着 elementType = "None"; if (match) { for (const [sname, name] of Object.entries(element_dic)) { if (match[0].includes(sname)) { elementType = element_dic[sname]; break; } } } // targetType 目标 【DEBUG】测试功能,优先实现 targetType = ""; // specialEffect 特殊效果 【DEBUG】测试功能,优先实现 specialEffect = ""; return new Action(dealDamage, instantDamage, clickTwice, damage, residueDegree, consumption, effectType, elementType, targetType, specialEffect); } /** * * 读取并解析本地的卡牌信息(assets/card_dic.json) * * @returns {Promise} */ async function parseCardData() { const text = file.readTextSync("assets/card_dic.json"); try { // 解析 JSON 字符串为对象 let parsedObject = JSON.parse(text); return parsedObject; } catch (error) { console.error("解析失败:", error); return null; } } /** * * 判断当前界面是否为初始手牌界面 * * @returns {Promise} */ async function isUiStartingHand(moveMouse = true) { const ocrRo = RecognitionObject.Ocr(844, 167, 232, 65); if (moveMouse) { moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR } await sleep(200); const ro1 = captureGameRegion(); let ocr = ro1.Find(ocrRo); // 当前页面OCR ro1.dispose(); if (ocr.isExist() && ocr.text === "初始手牌") { return true; } else { return false; } } /** * * 判断当前界面是否为重投骰子界面 * * @returns {Promise} */ async function isUiReroll(moveMouse = true) { const ocrRo = RecognitionObject.Ocr(844, 167, 232, 65); if (moveMouse) { moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR } await sleep(200); const ro2 = captureGameRegion(); let ocr = ro2.Find(ocrRo); // 当前页面OCR ro2.dispose(); if (ocr.isExist() && ocr.text === "重投骰子") { return true; } else { return false; } } /** * * 判断当前界面是否为出战角色选择界面【首次选择】【DEBUG】 * * @returns {Promise} */ async function isUiChoice(moveMouse = true) { const ocrRo = RecognitionObject.Ocr(1766, 851, 117, 37); if (moveMouse) { moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR } await sleep(200); const ro3 = captureGameRegion(); let ocr = ro3.Find(ocrRo); // 当前页面OCR ro3.dispose(); if (ocr.isExist() && ocr.text === "出战角色") { return true; } else { return false; } } /** * * 判断当前界面是否为结束界面 * * @returns {Promise} */ async function isUiFinish(moveMouse = true) { const ocrRo = RecognitionObject.Ocr(763, 101, 394, 381); if (moveMouse) { moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR } await sleep(200); const ro4 = captureGameRegion(); let ocr = ro4.Find(ocrRo); // 当前页面OCR ro4.dispose(); if (ocr.isExist() && ocr.text.includes("对局胜利")) { return "win"; } else if (ocr.isExist() && ocr.text.includes("对局失败")){ return "lose"; } else { // 不是结束界面 return "none"; } } /** * * 判断当前界面是否为对局界面 * * @returns {Promise} */ async function isUiMain(moveMouse = true) { const slide_barRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic["setting"]), 1834, 15, 66, 66); if (moveMouse) { moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR } await sleep(100); const ro5 = captureGameRegion(); let slide_bar = ro5.Find(slide_barRo); // 当前页面模板匹配 ro5.dispose(); if (slide_bar.isExist()) { return true; } else { return false; } } /** * * OCR识别当前点开的卡详情的卡名 * * @param mode 决定卡名的位置(例如初始手牌的卡详情和局内的卡详情位置不同) * @returns {Promise<*|boolean>} 识别成功返回字符串,否则返回false */ async function ocrCardName(mode="main") { const ocrRo_StartingHand = RecognitionObject.Ocr(58, 112, 339, 53); const ocrRo_Main = RecognitionObject.Ocr(311, 115, 341, 50); moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR await sleep(10); const ro6 = captureGameRegion(); let ocr; if (mode === "StartingHand") { ocr = ro6.Find(ocrRo_StartingHand); // 当前页面OCR } else if (mode === "main") { ocr = ro6.Find(ocrRo_Main); // 当前页面OCR } ro6.dispose(); if (ocr.isExist()) { return ocr.text; } else { return false; } } /** * * 获取当前的骰子类型和数量(自动识别对局界面和投掷界面), * * @param keepDice 要保留的骰子(投掷界面生效) * @param threshold 模板匹配容差(实测0.85-0.9较为准确) * @returns {Promise} */ async function getDiceMsg(keepDice = [], threshold = 0.85) { let dice_dic = {}; const diceNames = ["Pyro", "Hydro", "Anemo", "Electro", "Dendro", "Cryo", "Geo", "Omni"]; let region, keyPrefix; if (await isUiReroll()) { region = { x: 553, y: 330, w: 819, h: 411 }; keyPrefix = "Roll"; } else if (await isUiMain()) { region = { x: 1848, y: 177, w: 38, h: 737 }; keyPrefix = "Main"; } else { log.error(`当前界面错误,无法识别骰子信息...`); return false; } moveMouseTo(1555, 860); // 移走鼠标,防止干扰识别 await sleep(100); let gameRegion = captureGameRegion(); // 捕获一次游戏区域,避免重复调用 for (let i = 0; i < diceNames.length; i++) { let name = diceNames[i]; const key = keyPrefix + name; let diceRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic[key]), region.x, region.y, region.w, region.h); diceRo.threshold = threshold; let dice = gameRegion.FindMulti(diceRo); if (dice.count !== 0) { dice_dic[name] = dice.count; } if (keepDice.length !== 0 && !(keepDice.includes(name)) && dice.count !== 0) { // 排除不保留的骰子 for (let j = 0; j < dice.count; j++) { await sleep(200); dice[j].Click(); } } } gameRegion.dispose(); log.info(`[DEBUG] 剩余元素骰: ${Object.keys(dice_dic)} | ${Object.values(dice_dic)}`); return dice_dic; } /** * * 识别当前手牌数量(需要位于对局界面) * * @returns {Promise} */ async function getHandNum() { const HandNumNames = ["Hand5", "Hand6", "Hand7", "Hand8", "Hand9", "Hand10"]; click(967, 1041); // 点击手牌区域 await sleep(600); // 等待手牌统计数字出现 moveMouseTo(1791, 917); // 移走鼠标,防止干扰识别 await sleep(100); let gameRegion = captureGameRegion(); // 捕获一次游戏区域 for (let i = 0; i < HandNumNames.length; i++) { let numRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic[HandNumNames[i]]), 1463, 700, 439, 124); let num = gameRegion.Find(numRo); gameRegion.dispose(); if (num.isExist()) return parseInt(HandNumNames[i].slice(4), 10); } // 4 click(776, 920); await sleep(200); if (await ocrCardName() !== false) return 4; await sleep(500); click(967, 1041); // 点击手牌区域 await sleep(200); // 3 click(859, 920); await sleep(200); if (await ocrCardName() !== false) return 3; await sleep(500); click(967, 1041); // 点击手牌区域 await sleep(200); // 2 click(965, 920); await sleep(200); if (await ocrCardName() !== false) return 2; await sleep(500); click(967, 1041); // 点击手牌区域 await sleep(200); // 1 click(1061, 920); await sleep(200); if (await ocrCardName() !== false) return 1; // 0 click(1190, 545); // 点击屏幕中间靠右复位 return 0; } /** * * 识别手牌的具体信息 * * @param total 手牌总数 * @param extraWaitTime 额外等待时间 * @returns {Promise} */ async function getHandMsg(total, extraWaitTime = 0) { const waitTime = 100 + extraWaitTime; let HandMsgDic = {}; // 点击屏幕中间复位 click(1190, 545); // 点击屏幕中间靠右复位 await sleep(waitTime); // 点击手牌区域 click(967, 1041); await sleep(waitTime); for (const [serial, pos] of Object.entries(stateHand_dic[total])) { click(pos[0], pos[1]); await sleep(waitTime); let card_name = await ocrCardName(); // 实例化该牌的类并添加到字典 HandMsgDic[serial] = await createCardFromDict(card_name); } return HandMsgDic; } /** * 选择出战卡牌(切换出战卡牌) * * @param card_name 要切换的卡牌名 * @returns {Promise} */ async function setActiveCard(card_name, extraWaitTime = 0) { const waitTime = 100 + extraWaitTime; // 定义下方的两个点击坐标 const coords = [ { x: 750, y: 720 }, // 1 { x: 960, y: 720 }, // 2 { x: 1175, y: 720 } // 3 ]; // 循环 3 次,依次点击不同的坐标(不足时取余) for (let i = 0; i < 3; i++) { const { x, y } = coords[i % coords.length]; click(x, y); await sleep(waitTime); if (await ocrCardName() === card_name) { click(1825, 960); await sleep(500); // 局内切换(使用默认元素) click(1825, 960); // 局内切换(使用默认元素) // 如果满足条件则可以选择跳出循环 break; } } } /** * * 判断当前为哪方的行动回合 * * @param moveMouse 是否移开鼠标(防止模板匹配受影响) * @returns {Promise} 预期三种返回,我方,敌方,无 */ async function getActingTurn(moveMouse = true) { let userRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic["userTurn"]), 64, 518, 33, 42); let enemyRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic["enemyTurn"]), 64, 518, 33, 42); if (moveMouse) { moveMouseTo(1555, 860); // 移走鼠标,防止干扰OCR } let gameRegion = captureGameRegion(); // 捕获一次游戏区域 let userTurn = gameRegion.Find(userRo); let enemyTurn = gameRegion.Find(enemyRo); gameRegion.dispose(); if (userTurn.isExist()) { return "user"; } else if (enemyTurn.isExist()) { return "enemy"; } else { return "none"; } } /** * * 获取单张角色牌的充能情况 * * @param target 识别的卡牌所处的阵营(我方user,敌方enemy) * @param serial 卡牌的序号(从1开始,从左到右) * @param total 该阵营牌桌上角色牌总数 * @param gameRegion 传入的游戏截图 * @returns {Promise<*[]>} [充能数,未充能数] */ async function getCharge(target, serial, total, gameRegion, threshold = 0.7) { let target_msg = `${target}${total}${serial}`; // 目标卡牌信息,例如:enemy31, enemy42, user32 let charge, uncharge; let width = chargeArea_dic["size"][0]; let height = chargeArea_dic["size"][1]; for (const [name, pos] of Object.entries(chargeArea_dic)) { if (name === target_msg) { let chargeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic["charge"]), pos[0], pos[1], width, height); let unchargeRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic["uncharge"]), pos[0], pos[1], width, height); chargeRo.threshold = threshold unchargeRo.threshold = threshold let charge = gameRegion.FindMulti(chargeRo); let uncharge = gameRegion.FindMulti(unchargeRo); return [charge.count, uncharge.count]; // [充能数,未充能数] } } } /** * * 识别角色牌血量(OCR和模板匹配),调用此方法先需要等待2s * * @param target 识别的卡牌所处的阵营(我方user,敌方enemy) * @param serial 卡牌的序号(从1开始,从左到右) * @param total 该阵营牌桌上角色牌总数 * @param gameRegion 传入的游戏截图 * @returns {Promise} */ async function getCardHp(target, serial, total, gameRegion) { let target_msg = `${target}${total}${serial}`; // 目标卡牌信息,例如:enemy31, enemy42, user32 // 分别检测出战角色血量位置和未出战角色血量位置 let ocr1Ro = RecognitionObject.Ocr(hpArea_dic[target_msg][0], hpArea_dic[target_msg][1], hpArea_dic["size"][0], hpArea_dic["size"][1]); let ocr2Ro = RecognitionObject.Ocr(hpArea_dic[target_msg][0], hpArea_dic[target_msg][2], hpArea_dic["size"][0], hpArea_dic["size"][1]); let ocr1Result = gameRegion.Find(ocr1Ro); let ocr2Result = gameRegion.Find(ocr2Ro); let hp = 0 if (ocr1Result.isExist() || ocr2Result.isExist()) { try { hp = ocr1Result.isExist() ? parseInt(ocr1Result.text, 10): parseInt(ocr2Result.text, 10); return hp; } catch (error) { log.error(`角色牌: ${target_msg} 的血量OCR信息转换错误...\nmsg: ${error}`); return hp; } } else { // 未识别到血量信息 let disableRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(pic_path_dic["disable"]), stateArea_dic[target_msg][0], stateArea_dic[target_msg][1], stateArea_dic["size"][0], stateArea_dic["size"][1]); if (gameRegion.Find(disableRo).isExist()) { // 角色牌处于阵亡状态 return 0; } else { // 识别有误【DEBUG】暂且返回0,后续使用boolean return 0; } } } /** * * 获取单张角色牌的状态 * * @param target 识别的卡牌所处的阵营(我方user,敌方enemy) * @param serial 卡牌的序号(从1开始,从左到右) * @param total 该阵营牌桌上角色牌总数 * @param gameRegion 传入的游戏截图 * @returns {Promise<[]>} 列表,记录每种异常状态 ["Pyro", ...] */ async function getState(target, serial, total, gameRegion) { let target_msg = `${target}${total}${serial}`; // 目标卡牌信息,例如:enemy31, enemy42, user32 let stateList = []; for (const [name, path] of Object.entries(pic_path_dic)) { if (name.startsWith("State")) { let stateRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync(path), stateArea_dic[target_msg][0], stateArea_dic[target_msg][1], stateArea_dic["size"][0], stateArea_dic["size"][1]); let state = gameRegion.Find(stateRo); if (state.isExist()) stateList.push(name.slice(5)); // if (state.isExist()) log.info(`${target_msg}|${name}`); } } return stateList; } /** * * 模拟鼠标拖动操作 * * @param startX * @param startY * @param endX * @param endY * @param extraWaitTime 额外等待时间 * @returns {Promise} */ async function mouseDrag(startX, startY, endX, endY, extraWaitTime = 0) { const durationMs = 100 + extraWaitTime; const events = []; const totalDeltaX = endX - startX; const totalDeltaY = endY - startY; // 计算总移动距离(曼哈顿距离) const totalDistance = Math.abs(totalDeltaX) + Math.abs(totalDeltaY); // 按每步最大合位移10计算步数(至少1步) const steps = Math.max(1, Math.ceil(totalDistance / 10)); // 生成移动事件 for (let i = 1; i <= steps; i++) { const progress = i / steps; const currentX = startX + totalDeltaX * progress; const currentY = startY + totalDeltaY * progress; // 计算时间戳(均匀分布) const timestamp = Math.round((durationMs * i) / (steps + 1)); events.push({ type: 2, mouseX: Math.round(currentX), mouseY: Math.round(currentY), time: timestamp }); } // 添加起始事件(按下) events.unshift({ type: 4, mouseX: startX, mouseY: startY, mouseButton: "Left", time: 0 }); // 添加结束事件(抬起) events.push({ type: 5, mouseX: endX, mouseY: endY, mouseButton: "Left", time: durationMs }); let jsonObject = { macroEvents: events, info: { name: "", description: "", x: 0, y: 0, width: 1920, height: 1080, recordDpi: 1.25 } }; await keyMouseScript.run(JSON.stringify(jsonObject)); return true; } /** * * 模拟打出手牌 * * @param serial 要打出的手牌序号(从1开始,从左到右) * @param total 手牌总数 * @returns {Promise} */ async function dragCard(serial, total, extraWaitTime = 0) { let waitTime = 100 + extraWaitTime; // 点击屏幕中间复位 click(1190, 545); // 点击屏幕中间靠右复位 await sleep(waitTime); // 点击手牌区域 click(967, 1041); let x = stateHand_dic[total][serial][0]; let y = stateHand_dic[total][serial][1]; await sleep(waitTime); // 拖动并打出手牌【DEBUG】额外选择不应在此方法内执行,应另写方法 await mouseDrag(x, y, x, y - 350, extraWaitTime); return true; } /** * * 释放角色牌技能(无技能可用性判断) * * @param skill_id 技能序号(从0开始) * @param card_name 角色牌名称 * @param extraWaitTime 额外等待时间 * @returns {Promise} */ async function useSkill(skill_id, card_name, extraWaitTime = 0) { let waitTime = 100 + extraWaitTime; let skill_list = card_dic[card_name]["skills"]; click(1190, 545); // 点击屏幕中间靠右复位 await sleep(waitTime); if (skill_list.length === 3 || (skill_list.length == 4 && skill_list[3]["type"] === "被动技能")) { click(skillPosition_dic[skill_id + 1][0], skillPosition_dic[skill_id + 1][1]); await sleep(500 + waitTime); click(skillPosition_dic[skill_id + 1][0], skillPosition_dic[skill_id + 1][1]); return true; } else if (skill_list.length === 4) { click(skillPosition_dic[skill_id][0], skillPosition_dic[skill_id][1]); await sleep(500 + waitTime); click(skillPosition_dic[skill_id][0], skillPosition_dic[skill_id][1]); return true; } else { log.warn(`角色牌技能读取错误: ${card_name}(${skill_id})`); return false } } /** * * 判断剩余骰子是否足够释放技能,是否需要烧牌,要烧几张【DEBUG】部分角色消耗可变,后续需要加入游戏内检测 * * @param skill_id 技能序号(从0开始) * @param game 实例化的Game类,需要含有完整的局面数据(需要提前调用Game.get_state()) * @param dice_dic 当前剩余的元素骰(使用getDiceMsg()获取) * @returns {Promise<{isEnough: boolean, needDic: {}}>} */ async function calculateSkillCost(skill_id, game, dice_dic) { let charge = 0; let cost_dic = {}; let element_type; let all_dice_num = 0; let result_dic = { "isEnough": true, "needNum": 0 }; // 获取角色牌的当前充能和技能消耗信息 for (const [id, msg] of Object.entries(game.userCards)) { log.info(`[TEMP] ${msg["name"]} | ${game.userMainCard}`); if (msg["name"] === game.userMainCard) { charge = game.userCharge[id][0]; log.info(`[DEBUG] 角色当前技能(${msg["object"].skills[skill_id]["name"]})(skill_id: ${skill_id})`); log.info(`[DEBUG] 角色牌技能信息 ${Object.keys(msg["object"].skills[skill_id]["cost"])} | ${Object.values(msg["object"].skills[skill_id]["cost"])}`) cost_dic = msg["object"].skills[skill_id]["cost"]; for (const [cn_name, en_name] of Object.entries(element_dic)) { if (msg["object"].element.includes(cn_name)) { element_type = en_name; } } break; } } // 获取剩余的骰子总数 for (const [name, num] of Object.entries(dice_dic)) { all_dice_num += num; } if (all_dice_num < Object.values(cost_dic).reduce((a, b) => a + b, 0)) { // 如果剩余骰子数不满足要求 result_dic["isEnough"] = false; result_dic["needNum"] = -1; } else { // 计算需求是否满足(遍历角色的技能需求字典)[DEBUG] 可能存在逻辑漏洞,万能元素和指定元素应当合并判断 let all_active_dic_num = 0; let allNum = 0; log.info(`[TEMP] ${Object.keys(cost_dic)} | ${Object.values(cost_dic)}`); for (const [name, num] of Object.entries(cost_dic)) { log.info(`[DEBUG] name: ${name}`); if (name.includes("元素") && name !== "万能元素") { // 需求为元素骰【DEBUG】此处不检测需要什么元素,而是默认使用角色牌的元素 allNum += num; log.info(`[DEBUG] dice_dic_keys: ${Object.keys(dice_dic)} | element_type: ${element_type}`); if (Object.keys(dice_dic).includes(element_type)) { // 如果有当前角色牌所需的元素骰子 all_active_dic_num += dice_dic[element_type]; } if (Object.keys(dice_dic).includes("Omni")){ // 若有万能骰子 all_active_dic_num += dice_dic["Omni"]; } } else if (name.includes("充能")) { // 需求为充能 let difference = game.userCharge[game.userMainCardId][0] - num; // 计算当前充能和需求充能的差值 if (difference > 0) { // 充能不足 result_dic["isEnough"] = false; result_dic["needNum"] = -1; // 无法通过元素调和解决【DEBUG】后续可通过Action进行手牌的牌型分析解决 } } } let difference = allNum - all_active_dic_num; // 计算差值(所需元素骰和已有元素骰) if (difference > 0) { result_dic["isEnough"] = false; result_dic["needNum"] = difference; } } return result_dic; } /** * * 结束我方当前行动回合(需要处于我方回合) * * @param extraWaitTime 额外等待时间 * @returns {Promise} */ async function terminateTurn(extraWaitTime = 0) { let waitTime = 200 + extraWaitTime; // let ocrRo = RecognitionObject.Ocr(274, 519, 140, 42); while (await getActingTurn() !== "user") { // 确保为我方回合 await sleep(500); } click(79, 539); // 点击结束回合按钮【DEBUG】有点冗余了,后续得改改 await sleep(500 + waitTime); click(79, 539); // 点击结束回合按钮 await sleep(500 + waitTime); click(79, 539); // 点击结束回合按钮 await sleep(500 + waitTime); click(79, 539); // 点击结束回合按钮 return null; // while (true) { // 等待回合结束出现(容易失去OCR目标陷入死循环) // // await sleep(waitTime); // let ocr = captureGameRegion().Find(ocrRo); // if (ocr.isExist() && ocr.text === "回合结束") { // ocr.Click(); // await sleep(waitTime); // // ocr.Click(); // // await sleep(waitTime); // break; // } // } } /** * * 切换角色(根据ID切换)【DEBUG】修改建议:改为模板匹配右下角启人图标 * * @param id 角色id(从左到右,从0开始) * @returns {Promise} */ async function switchCard(id, extraWaitTime = 0) { let waitTime = 200 + extraWaitTime; let cardPosDic = { 0: [750, 720], 1: [960, 720], 2: [1175, 720] } click(1190, 545); // 点击屏幕中间靠右复位 // 复位 await sleep(waitTime); // 点击角色牌 click(cardPosDic[id][0], cardPosDic[id][1]); await sleep(waitTime); click(1820, 958); // 点击切人按钮 await sleep(500 + waitTime); click(1820, 958); // 再次点击切人按钮 } /** * * 元素调和(调和次数要大于等于手牌数)【DEBUG】后续改成指定要调和的牌 * * @param tuningTime 调和次数 * @param extraWaitTime 额外等待时间 * @returns {Promise} */ async function elementalTuning(tuningTime, extraWaitTime = 0) { let waitTime = 500 + extraWaitTime; let handCardNum = await getHandNum(); log.info(`当前手牌数量: ${handCardNum}`); click(1190, 545); // 点击屏幕中间靠右复位 // 复位 await sleep(waitTime); // 点击手牌区域 click(967, 1041); await sleep(waitTime + 500); for (let i = handCardNum; i > handCardNum - tuningTime; i--) { await mouseDrag(stateHand_dic[handCardNum][1][0], stateHand_dic[handCardNum][1][1], 1867, 518, waitTime + 500); await sleep(200 + waitTime); click(960, 948); // 点击 元素调和 await sleep(waitTime); } click(1190, 545); // 点击屏幕中间靠右复位 // 复位 await sleep(waitTime); } async function main() { // 【DEBUG】main的结构需要整理 // let testDic = await getHandMsg(5); // for (const [serial, card] of Object.entries(testDic)) { // log.info(`${serial}|${card.name}|${card.ctype}|${card.label}|${card.means}`); // } // return null; // await sleep(1000); // await dragCard(2, 6); // return null; // let action = createActionFromEffectText("附属角色受到圣遗物以外的治疗后:治疗我方受伤最多的角色1点。(每回合至多触发2次)"); // log.info(`${action.damage}`); // return null; let strategy = new Strategy(); await strategy.strategyOne(); } await main(); })();