Files
bettergi-scripts-list/repo/js/DSTCG/main.js
躁动的氨气 383232869a 规范识别范围,避免直接使用genshin.width (#2464)
* 规范识别范围,避免直接使用genshin.width
2025-12-09 13:20:43 +08:00

2521 lines
115 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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为x1、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 <skills.length; i++) {
if (skills[i]["type"] === "元素战技") {
// 计算消耗
costDic = await calculateSkillCost(skills[i]["id"], this.TcgGame, this.diceMsg);
if (costDic["isEnough"]) { // 如果能释放就释放
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
} else if (costDic["needNum"] === -1) { // 不满足释放条件,且无法通过元素调和的方式释放
// 结束当前回合【DEBUG】很极端的一种情况尚待测试
await terminateTurn();
} else { // 元素调和后释放
await elementalTuning(costDic["needNum"]);
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
}
break;
}
}
} else {
if (this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].weapon === "法器") { // 我方出战角色为法器角色
// 释放元素战技
for (let i = 0; i <skills.length; i++) {
if (skills[i]["type"] === "元素战技") {
// 计算消耗
costDic = await calculateSkillCost(skills[i]["id"], this.TcgGame, this.diceMsg);
if (costDic["isEnough"]) { // 如果能释放就释放
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
} else if (costDic["needNum"] === -1) { // 不满足释放条件,且无法通过元素调和的方式释放
// 结束当前回合【DEBUG】很极端的一种情况尚待测试
await terminateTurn();
} else { // 元素调和后释放
await elementalTuning(costDic["needNum"]);
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
}
break;
}
}
} 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 { // 我方出战角色使用附带元素附着的攻击(优先技能,避免使用元素爆发)
log.info(`[DEBUG] 待释放技能所需元素骰子详情: ${this.TcgGame.userCards[this.TcgGame.userMainCardId]["name"]}|${this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name}`);
let skills = this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].skills;
for (let i = 0; i < skills.length; i++) {
if (skills[i]["type"] !== "元素爆发" && skills[i]["effect_text"].includes("元素")) {
// 计算技能释放条件
costDic = await calculateSkillCost(skills[i]["id"], this.TcgGame, this.diceMsg);
if (costDic["isEnough"]) {
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
} else if (costDic["needNum"] === -1) {
await terminateTurn();
} else {
await elementalTuning(costDic["needNum"]);
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
}
break;
}
}
}
} else if (restHpDic[this.TcgGame.userMainCardId] <= 0) {
// 我方出战角色经元素反应检测后剩余血量小于等于0
this.TcgGame.calculateEachReactionEffect("enemy"); // 计算敌方受到我方的元素影响
if (this.TcgGame.enemyHp[this.TcgGame.enemyMainCardId] - this.TcgGame.effectDic[this.TcgGame.enemyMainCardId] <= 0) { // 敌方出战角色经元素反应检测后血量小于等于0
let skills = this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].skills
// 释放元素战技
for (let i = 0; i <skills.length; i++) {
if (skills[i]["type"] === "元素战技") {
// 计算消耗
costDic = await calculateSkillCost(skills[i]["id"], this.TcgGame, this.diceMsg);
if (costDic["isEnough"]) { // 如果能释放就释放
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
} else if (costDic["needNum"] === -1) { // 不满足释放条件,且无法通过元素调和的方式释放
// 结束当前回合【DEBUG】很极端的一种情况尚待测试
await terminateTurn();
} else { // 元素调和后释放
await elementalTuning(costDic["needNum"]);
await useSkill(skills[i]["id"], this.TcgGame.userCards[this.TcgGame.userMainCardId]["object"].name);
}
break;
}
}
} else {
await switchCard(Object.values(restHpDic).indexOf(Math.max(...Object.values(restHpDic))));
}
} else { // 理论上不可能执行该步骤
log.error("策略执行异常,结束回合");
await terminateTurn();
}
} else if (turnStatus === "enemy") { // 敌方回合
click(960, 540);
await sleep(1000);
} else {
click(1873, 47);
await sleep(1500);
}
await sleep(2000);
}
}
/**
* 策略模板
*/
async strategySerial() {
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 currentHandNum = await getHandNum();
// 计算元素影响
this.TcgGame.calculateEachReactionEffect("user");
// 计算元素影响后的我方角色牌剩余血量
let restHpDic = {};
for (const [id, num] of Object.entries(this.TcgGame.userHp)) {
restHpDic[id] = num - this.TcgGame.effectDic[id];
}
let diceNum = Object.values(this.diceMsg).reduce((acc, current) => 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<CharacterCard|SummonCard|Card|ActionCard>} 实例化的卡牌类
*/
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<Action>}
*/
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<any|null>}
*/
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<boolean>}
*/
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<boolean>}
*/
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<boolean>}
*/
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<string>}
*/
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<boolean>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>} 预期三种返回,我方,敌方,无
*/
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<number|boolean>}
*/
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<boolean>}
*/
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<boolean>}
*/
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<boolean>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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();
})();