feat(task): 培育中选择技能卡时优先选择游戏推荐的卡

This commit is contained in:
XcantloadX 2025-04-28 15:41:02 +08:00
parent a1f34e5f5f
commit 2727f08faa
9 changed files with 207 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

View File

@ -0,0 +1 @@
{"definitions":{"b0283997-7931-476d-a92f-d7569f6ea34c":{"name":"InPurodyuusu.TextRecommend","displayName":"おすすめ","type":"template","annotationId":"b0283997-7931-476d-a92f-d7569f6ea34c","useHintRect":false,"description":"技能卡选择对话框中的推荐卡片"}},"annotations":[{"id":"b0283997-7931-476d-a92f-d7569f6ea34c","type":"rect","data":{"x1":179,"y1":959,"x2":262,"y2":982}}]}

View File

@ -1,4 +1,6 @@
from .toolbar import toolbar_home, toolbar_menu
from .commu_event_buttons import CommuEventButtonUI, web2cv, DEFAULT_COLORS
from .common import WhiteFilter
from .scrollable import Scrollable, ScrollableIterator
from .scrollable import Scrollable, ScrollableIterator
from . import dialog
from . import badge

View File

@ -0,0 +1,96 @@
"""
badge 模块用于关联带附加徽章的 UI
例如培育中的课程按钮+SP 图标工作中分配偶像时偶像图标+好调图标
"""
from typing import Literal, NamedTuple
from kotonebot.util import Rect
BadgeCorner = Literal['lt', 'lm', 'lb', 'rt', 'rm', 'rb', 'mt', 'm', 'mb']
"""
Badge 位置
可选 ``['l', 'm', 'r']`` ``['t', 'm', 'b']`` 的组合
"""
class BadgeResult(NamedTuple):
object: Rect
badge: Rect | None
def match(
objects: list[Rect],
badges: list[Rect],
corner: BadgeCorner,
threshold_distance: float = float('inf')
) -> list[BadgeResult]:
"""
将对象与徽章匹配根据指定的角落位置
:param objects: 对象矩形列表
:param badges: 徽章矩形列表
:param corner: 徽章相对于对象的位置 'lt'左上'rb'右下
:param threshold_distance: 匹配的最大距离阈值超过此距离的匹配将被忽略
:return: 匹配结果列表
"""
# 将 rect 转换为中心点
def center(rect: Rect) -> tuple[int, int]:
return rect[0] + rect[2] // 2, rect[1] + rect[3] // 2
# 判断 badge 是否在 object 的指定角落位置
def is_in_corner(obj_rect: Rect, badge_center: tuple[int, int]) -> bool:
obj_center = center(obj_rect)
x_obj, y_obj = obj_center
x_badge, y_badge = badge_center
# 获取对象的边界
obj_left = obj_rect[0]
obj_right = obj_rect[0] + obj_rect[2]
obj_top = obj_rect[1]
obj_bottom = obj_rect[1] + obj_rect[3]
# 检查水平位置
if corner.startswith('l') and x_badge >= x_obj:
return False
if corner.startswith('r') and x_badge <= x_obj:
return False
if corner.startswith('m') and (x_badge < obj_left or x_badge > obj_right):
# 水平中间位置需要在对象的水平范围内
return False
# 检查垂直位置
if corner.endswith('t') and y_badge >= y_obj:
return False
if corner.endswith('b') and y_badge <= y_obj:
return False
if corner.endswith('m') and (y_badge < obj_top or y_badge > obj_bottom):
# 垂直中间位置需要在对象的垂直范围内
return False
return True
results = []
available_badges = badges.copy()
for obj_rect in objects:
obj_center = center(obj_rect)
target_badge = None
min_dist = float('inf')
target_index = -1
# 查找最近的符合条件的徽章
for i, badge_rect in enumerate(available_badges):
badge_center = center(badge_rect)
if is_in_corner(obj_rect, badge_center):
dist = ((badge_center[0] - obj_center[0]) ** 2 + (badge_center[1] - obj_center[1]) ** 2) ** 0.5
if dist < min_dist and dist <= threshold_distance:
min_dist = dist
target_badge = badge_rect
target_index = i
# 如果找到匹配的徽章,从可用徽章列表中移除
if target_badge is not None:
available_badges.pop(target_index)
results.append(BadgeResult(obj_rect, target_badge))
return results

View File

@ -14,7 +14,7 @@ from .p_drink import acquire_p_drink
from kotonebot.util import measure_time
from kotonebot.tasks.common import conf
from kotonebot.tasks.actions.loading import loading
from kotonebot.tasks.game_ui import CommuEventButtonUI, dialog
from kotonebot.tasks.game_ui import CommuEventButtonUI, dialog, badge
from kotonebot.tasks.actions.commu import handle_unread_commu
logger = getLogger(__name__)
@ -29,6 +29,7 @@ def acquire_skill_card():
it = Interval()
cards = None
card_clicked = False
target_card = None
while True:
device.screenshot()
@ -50,10 +51,26 @@ def acquire_skill_card():
return
cards = sorted(cards, key=lambda x: (x.position[0], x.position[1]))
logger.info(f"Found {len(cards)} skill cards")
# 判断是否有推荐卡
rec_badges = image.find_all(R.InPurodyuusu.TextRecommend)
rec_badges = [card.rect for card in rec_badges]
if rec_badges:
cards = [card.rect for card in cards]
matches = badge.match(cards, rec_badges, 'mb')
logger.debug("Recommend card badge matches: %s", matches)
# 选第一个推荐卡
target_match = next(filter(lambda m: m.badge is not None, matches), None)
if target_match:
target_card = target_match.object
else:
target_card = cards[0]
else:
logger.debug("No recommend badge found. Pick first card.")
target_card = cards[0].rect
continue
if not card_clicked:
logger.debug("Click first skill card")
device.click(cards[0].rect)
if not card_clicked and target_card is not None:
logger.debug("Click target skill card")
device.click(target_card)
card_clicked = True
sleep(0.2)
continue

View File

@ -569,7 +569,7 @@ def week_final_exam():
exam('final')
produce_end()
@action('执行 Regular 培育')
@action('执行 Regular 培育', screenshot_mode='manual-inherit')
def hajime_regular(week: int = -1, start_from: int = 1):
"""
Regular 模式
@ -602,7 +602,7 @@ def hajime_regular(week: int = -1, start_from: int = 1):
logger.info("Week %d started.", i + start_from)
w()
@action('执行 PRO 培育')
@action('执行 PRO 培育', screenshot_mode='manual-inherit')
def hajime_pro(week: int = -1, start_from: int = 1):
"""
PRO 模式
@ -636,7 +636,7 @@ def hajime_pro(week: int = -1, start_from: int = 1):
logger.info("Week %d started.", i + start_from)
w()
@action("执行 MASTER 培育")
@action("执行 MASTER 培育", screenshot_mode='manual-inherit')
def hajime_master(week: int = -1, start_from: int = 1):
"""
MASTER 模式

0
tests/__init__.py Normal file
View File

0
tests/kaa/__init__.py Normal file
View File

83
tests/kaa/test_badge.py Normal file
View File

@ -0,0 +1,83 @@
from unittest import TestCase
from kotonebot.tasks.game_ui.badge import match, BadgeResult
from kotonebot.util import Rect
def rect_from_center(x: int, y: int) -> Rect:
w, h = 20, 20
return x - w // 2, y - h // 2, w, h
class TestBadge(TestCase):
def test_match(self):
# 测试数据
# https://www.desmos.com/calculator/dsynum9p4i
objects = [
rect_from_center(125, 125),
rect_from_center(230, 230),
rect_from_center(320, 120),
]
badges = [
# 左上角徽章
rect_from_center(90, 160), # 对应 objects[0] 的左上
# 右下角徽章
rect_from_center(260, 200), # 对应 objects[1] 的右下
# 右上角徽章
rect_from_center(340, 130), # 对应 objects[2] 的右上
# 不匹配任何对象的徽章
rect_from_center(410, 410),
]
# 测试左上角匹配
results = match(objects, badges, 'lt', 50)
self.assertEqual(len(results), 3)
self.assertEqual(results[0].object, objects[0])
self.assertEqual(results[0].badge, badges[0])
self.assertEqual(results[1].object, objects[1])
self.assertIsNone(results[1].badge)
self.assertEqual(results[2].object, objects[2])
self.assertIsNone(results[2].badge)
# 测试右下角匹配
results = match(objects, badges, 'rb', 50)
self.assertEqual(len(results), 3)
self.assertEqual(results[0].object, objects[0])
self.assertIsNone(results[0].badge)
self.assertEqual(results[1].object, objects[1])
self.assertEqual(results[1].badge, badges[1])
self.assertEqual(results[2].object, objects[2])
self.assertIsNone(results[2].badge)
# 测试右上角匹配
results = match(objects, badges, 'rt', 50)
self.assertEqual(len(results), 3)
self.assertEqual(results[0].object, objects[0])
self.assertIsNone(results[0].badge)
self.assertEqual(results[1].object, objects[1])
self.assertIsNone(results[1].badge)
self.assertEqual(results[2].object, objects[2])
self.assertEqual(results[2].badge, badges[2])
# 测试没有匹配的情况
results = match(objects, [], 'lt', 50)
self.assertEqual(len(results), 3)
for result in results:
self.assertIsNone(result.badge)
# 测试空对象列表
results = match([], badges, 'lt', 50)
self.assertEqual(len(results), 0)
# 测试当多个徽章符合条件时,选择最近的一个
def test_match_with_multiple_badges(self):
# 测试数据
# https://www.desmos.com/calculator/pytdqaju4w
objects = [rect_from_center(125, 125)]
badges = [
rect_from_center(90, 90),
rect_from_center(80, 80),
rect_from_center(70, 70),
]
results = match(objects, badges, 'lb')
self.assertEqual(len(results), 1)
self.assertEqual(results[0].object, objects[0])
self.assertEqual(results[0].badge, badges[0])