feat(task): 培育中支持处理咨询

This commit is contained in:
XcantloadX 2025-04-24 17:05:34 +08:00
parent 1c4de8330c
commit 1c8621f026
16 changed files with 114 additions and 66 deletions

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 953 KiB

After

Width:  |  Height:  |  Size: 953 KiB

View File

@ -0,0 +1 @@
{"definitions":{"8ded6c98-85ea-4858-a66d-4fc8caecb7c5":{"name":"InPurodyuusu.ButtonIconOuting","displayName":"行动按钮图标 外出(おでかけ)","type":"template","annotationId":"8ded6c98-85ea-4858-a66d-4fc8caecb7c5","useHintRect":false},"d83f338d-dde3-494b-9bea-cae511e3517c":{"name":"InPurodyuusu.ButtonIconConsult","displayName":"行动按钮图标 咨询(相談)","type":"template","annotationId":"d83f338d-dde3-494b-9bea-cae511e3517c","useHintRect":false}},"annotations":[{"id":"8ded6c98-85ea-4858-a66d-4fc8caecb7c5","type":"rect","data":{"x1":233,"y1":962,"x2":316,"y2":1037}},{"id":"d83f338d-dde3-494b-9bea-cae511e3517c","type":"rect","data":{"x1":405,"y1":963,"x2":488,"y2":1043}}]}

View File

@ -1 +0,0 @@
{"definitions":{"8ded6c98-85ea-4858-a66d-4fc8caecb7c5":{"name":"InPurodyuusu.ButtonIconOuting","displayName":"行动按钮图标 外出(おでかけ)","type":"template","annotationId":"8ded6c98-85ea-4858-a66d-4fc8caecb7c5","useHintRect":false}},"annotations":[{"id":"8ded6c98-85ea-4858-a66d-4fc8caecb7c5","type":"rect","data":{"x1":233,"y1":962,"x2":316,"y2":1037}}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

View File

@ -0,0 +1 @@
{"definitions":{"23d88465-65d9-4718-8725-8dbf0a98a5a4":{"name":"InPurodyuusu.IconTitleConsult","displayName":"「相談」页面左上角图标","type":"template","annotationId":"23d88465-65d9-4718-8725-8dbf0a98a5a4","useHintRect":false},"1fb9bd7a-52f6-43c6-9b13-a05b66ecae42":{"name":"InPurodyuusu.PointConsultFirstItem","displayName":"「相談」中第一个物品位置","type":"hint-point","annotationId":"1fb9bd7a-52f6-43c6-9b13-a05b66ecae42","useHintRect":false},"9fd0753f-c607-4d49-82d1-40bda27e014f":{"name":"InPurodyuusu.ButtonEndConsult","displayName":"相談 结束按钮","type":"template","annotationId":"9fd0753f-c607-4d49-82d1-40bda27e014f","useHintRect":false}},"annotations":[{"id":"23d88465-65d9-4718-8725-8dbf0a98a5a4","type":"rect","data":{"x1":74,"y1":79,"x2":150,"y2":145}},{"id":"1fb9bd7a-52f6-43c6-9b13-a05b66ecae42","type":"point","data":{"x":123,"y":550}},{"id":"9fd0753f-c607-4d49-82d1-40bda27e014f","type":"rect","data":{"x1":587,"y1":1062,"x2":703,"y2":1109}}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

View File

@ -0,0 +1 @@
{"definitions":{"4096cffa-a889-4622-852e-760fc7022d93":{"name":"InPurodyuusu.ButtonIconExchange","displayName":"交换按钮的图标","type":"template","annotationId":"4096cffa-a889-4622-852e-760fc7022d93","useHintRect":false}},"annotations":[{"id":"4096cffa-a889-4622-852e-760fc7022d93","type":"rect","data":{"x1":258,"y1":1066,"x2":303,"y2":1108}}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

View File

@ -0,0 +1 @@
{"definitions":{"25f00ee3-8dfe-42d1-a67e-191fa5c3df4b":{"name":"InPurodyuusu.TextExchangeConfirm","displayName":"交換確認","type":"template","annotationId":"25f00ee3-8dfe-42d1-a67e-191fa5c3df4b","useHintRect":false,"description":"咨询中,购买确认对话框的标题"}},"annotations":[{"id":"25f00ee3-8dfe-42d1-a67e-191fa5c3df4b","type":"rect","data":{"x1":53,"y1":612,"x2":191,"y2":652}}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -283,6 +283,7 @@ class ProduceAction(Enum):
STUDY = 'study'
ALLOWANCE = 'allowance'
REST = 'rest'
CONSULT = 'consult'
@property
def display_name(self):
@ -295,6 +296,7 @@ class ProduceAction(Enum):
ProduceAction.STUDY: '文化课(授業)',
ProduceAction.ALLOWANCE: '活动支给(活動支給)',
ProduceAction.REST: '休息',
ProduceAction.CONSULT: '咨询(相談)',
}
return MAP[self]
@ -323,7 +325,6 @@ class ProduceConfig(ConfigBaseModel):
idols: list[str] = []
"""
要培育偶像的 IdolCardSkin.id将会按顺序循环选择培育
若未选择任何偶像则使用游戏默认选择的偶像为上次培育偶像
"""
memory_sets: list[int] = []
"""要使用的回忆编成编号,从 1 开始。将会按顺序循环选择使用。"""

View File

@ -18,7 +18,8 @@ from ..produce.non_lesson_actions import (
enter_allowance, allowance_available,
study_available, enter_study,
is_rest_available, rest,
outing_available, enter_outing
outing_available, enter_outing,
consult_available, enter_consult
)
logger = logging.getLogger(__name__)
@ -67,6 +68,7 @@ def handle_recommended_action(final_week: bool = False) -> ProduceAction | None:
R.InPurodyuusu.TextSenseiTipVocal,
R.InPurodyuusu.TextSenseiTipVisual,
R.InPurodyuusu.TextSenseiTipRest,
R.InPurodyuusu.TextSenseiTipConsult,
]):
break
it.wait()
@ -94,6 +96,9 @@ def handle_recommended_action(final_week: bool = False) -> ProduceAction | None:
elif result.index == 3:
rest()
return ProduceAction.REST
elif result.index == 4:
enter_consult()
return ProduceAction.CONSULT
else:
return None
# 点击课程
@ -469,6 +474,10 @@ def handle_action(action: ProduceAction, final_week: bool = False) -> ProduceAct
if allowance_available():
enter_allowance()
return ProduceAction.ALLOWANCE
case ProduceAction.CONSULT:
if consult_available():
enter_consult()
return ProduceAction.CONSULT
case _:
logger.warning("Unknown action: %s", action)
return None
@ -493,7 +502,7 @@ def week_normal(week_first: bool = False):
match action:
case (
ProduceAction.REST |
ProduceAction.OUTING | ProduceAction.STUDY | ProduceAction.ALLOWANCE
ProduceAction.OUTING | ProduceAction.STUDY | ProduceAction.ALLOWANCE | ProduceAction.CONSULT
):
# 什么都不需要做
pass
@ -521,7 +530,8 @@ def week_final_lesson():
match action:
case (
ProduceAction.REST |
ProduceAction.OUTING | ProduceAction.STUDY | ProduceAction.ALLOWANCE
ProduceAction.OUTING | ProduceAction.STUDY | ProduceAction.ALLOWANCE |
ProduceAction.CONSULT
):
# 什么都不需要做
pass
@ -828,44 +838,4 @@ if __name__ == '__main__':
init_context(config_type=BaseConfig)
manual_context().begin()
debug.auto_save_to_folder = 'dumps'
debug.enabled = True
# hajime_regular(start_from=1)
# pf = Profiler('profiler')
# pf.begin()
# # do_produce(conf().produce.idols[0], 'pro')
# practice()
# hajime_pro(start_from=16)
# pf.end()
# pf.snakeviz()
# while True:
# cards = obtain_cards()
# print(cards)
# sleep(1)
# practice()
# week_mid_exam()
# week_final_exam()
# exam('final')
# produce_end()
# hajime_pro(start_from=16)
# exam('mid')
stage = (detect_produce_scene())
hajime_from_stage(stage, 'pro', 0)
# click_recommended_card(card_count=skill_card_count())
# exam('mid')
# hajime_regular(start_from=7)
# import cv2
# while True:
# img = device.screenshot()
# cv2.imshow('123', img)
# cv2.waitKey(1)
debug.enabled = True

View File

@ -5,12 +5,14 @@
"""
from logging import getLogger
from kotonebot.tasks.game_ui import dialog
from .. import R
from ..common import conf
from ..produce.common import fast_acquisitions
from ..game_ui.commu_event_buttons import CommuEventButtonUI
from kotonebot.util import Interval
from kotonebot.util import Countdown, Interval
from kotonebot.errors import UnrecoverableError
from kotonebot import device, image, action, sleep
from kotonebot.backend.dispatch import SimpleDispatcher
@ -32,6 +34,14 @@ def study_available():
# [screenshots/produce/action_study1.png]
return image.find(R.InPurodyuusu.ButtonIconStudy) is not None
@action('检测是否可以执行相談')
def consult_available():
"""
判断是否可以执行相談
"""
return image.find(R.InPurodyuusu.ButtonIconConsult) is not None
# TODO: 把进入授業的逻辑和执行授業的逻辑分离
@action('执行授業')
def enter_study():
"""
@ -131,6 +141,87 @@ def enter_allowance():
it.wait()
logger.info("活動支給 completed.")
# TODO: 将逻辑用循环改写
@action('执行相談', screenshot_mode='manual-inherit')
def enter_consult():
"""
执行相談
前置条件位于行动页面且所有行动按钮清晰可见 \n
结束状态位于行动页面
"""
logger.info("Executing 相談.")
logger.info("Double clicking on 相談.")
device.screenshot()
device.double_click(image.expect(R.InPurodyuusu.ButtonIconConsult), interval=1)
# 等待进入页面
while not image.find(R.InPurodyuusu.IconTitleConsult):
device.screenshot()
logger.debug("Waiting for 相談 screen.")
fast_acquisitions()
# # 尝试固定购买第一个物品
# device.click(R.InPurodyuusu.PointConsultFirstItem)
# sleep(0.5)
# device.click(image.expect(R.InPurodyuusu.ButtonIconExchange))
# # 等待弹窗
# timeout_cd = Countdown(sec=5).start()
# while not timeout_cd.expired():
# if dialog.yes():
# break
# # 结束
# while not image.find(R.InPurodyuusu.ButtonEndConsult):
# fast_acquisitions()
# device.click(image.expect_wait(R.InPurodyuusu.ButtonEndConsult))
# # 可能会弹出确认对话框
# timeout_cd.reset().start()
# while not timeout_cd.expired():
# dialog.yes()
device.click(R.InPurodyuusu.PointConsultFirstItem)
sleep(0.3)
it = Interval()
wait_purchase_cd = Countdown(sec=5)
exit_cd = Countdown(sec=5)
purchase_clicked = False
purchase_confirmed = False
exit_clicked = False
while True:
device.screenshot()
it.wait()
if wait_purchase_cd.expired():
# 等待购买确认对话框超时后直接认为购买完成
purchase_confirmed = True
if dialog.yes():
if purchase_clicked:
purchase_confirmed = True
continue
elif purchase_confirmed:
continue
elif exit_clicked:
break
if image.find(R.InPurodyuusu.ButtonIconExchange, colored=True):
device.click()
purchase_clicked = True
continue
if purchase_confirmed and image.find(R.InPurodyuusu.ButtonEndConsult):
device.click()
exit_clicked = True
exit_cd.start()
continue
# 等待退出对话框超时,直接退出
if exit_cd.expired():
break
if not purchase_confirmed:
device.click(R.InPurodyuusu.PointConsultFirstItem)
# 处理不能购买的情况(超时)
# TODO: 应当检测画面文字/图标而不是用超时
wait_purchase_cd.start()
logger.info("相談 completed.")
@action('判断是否可以休息')
def is_rest_available():
"""
@ -213,22 +304,4 @@ if __name__ == '__main__':
from kotonebot.backend.context import manual_context, init_context
init_context()
manual_context().begin()
# 获取三个选项的内容
ui = CommuEventButtonUI()
buttons = ui.all()
if not buttons:
raise UnrecoverableError("Failed to find any buttons.")
# 选中 +30 的选项
target_btn = next((btn for btn in buttons if btn.description == '+30'), None)
if target_btn is None:
logger.error("Failed to find +30 option. Pick the first button instead.")
target_btn = buttons[0]
# 固定点击 Vi. 选项
logger.debug('Clicking "%s".', target_btn.description)
if target_btn.selected:
device.click(target_btn)
else:
device.double_click(target_btn)
while fast_acquisitions() is None:
logger.info("Waiting for acquisitions finished.")
logger.info("授業 completed.")
enter_consult()

View File

@ -217,6 +217,7 @@ class Countdown:
def reset(self):
self.start_time = time.time()
return self
class Stopwatch:
def __init__(self):