From fc8c45690264be41081df0e286a7be2d8ac4f486 Mon Sep 17 00:00:00 2001 From: XcantloadX <3188996979@qq.com> Date: Sat, 8 Mar 2025 17:33:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(task):=20=E6=94=AF=E6=8C=81=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=E5=9F=B9=E8=82=B2=E4=B8=AD=E8=A1=8C=E5=8A=A8=E4=BC=98?= =?UTF-8?q?=E5=85=88=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in_purodyuusu/produce_action_1.png.json | 2 +- kotonebot/tasks/actions/in_purodyuusu.py | 157 ++++++++++++++---- kotonebot/tasks/common.py | 51 +++++- kotonebot/ui/gr.py | 17 +- 4 files changed, 188 insertions(+), 39 deletions(-) diff --git a/kotonebot-resource/sprites/jp/in_purodyuusu/produce_action_1.png.json b/kotonebot-resource/sprites/jp/in_purodyuusu/produce_action_1.png.json index d2aacb1..d7c5c69 100644 --- a/kotonebot-resource/sprites/jp/in_purodyuusu/produce_action_1.png.json +++ b/kotonebot-resource/sprites/jp/in_purodyuusu/produce_action_1.png.json @@ -1 +1 @@ -{"definitions":{"be6836bd-ee42-432b-9166-469c74f32f0b":{"name":"InPurodyuusu.BoxWeeksUntilExam","displayName":"考试剩余周","type":"hint-box","annotationId":"be6836bd-ee42-432b-9166-469c74f32f0b","useHintRect":false,"description":"培育中左上角的下次考试剩余周数"}},"annotations":[{"id":"be6836bd-ee42-432b-9166-469c74f32f0b","type":"rect","data":{"x1":11,"y1":8,"x2":237,"y2":196}}]} \ No newline at end of file +{"definitions":{"be6836bd-ee42-432b-9166-469c74f32f0b":{"name":"InPurodyuusu.BoxWeeksUntilExam","displayName":"考试剩余周","type":"hint-box","annotationId":"be6836bd-ee42-432b-9166-469c74f32f0b","useHintRect":false,"description":"培育中左上角的下次考试剩余周数"},"d6b64759-26b7-45b1-bf8e-5c0d98611e0d":{"name":"InPurodyuusu.TextActionVocal","displayName":"Vo. レッスン","type":"template","annotationId":"d6b64759-26b7-45b1-bf8e-5c0d98611e0d","useHintRect":false,"description":"培育 行动页面 声乐课程按钮文字"},"303cccc1-c674-4d3a-8c89-19ea729fdbef":{"name":"InPurodyuusu.TextActionDance","displayName":"Da. レッスン","type":"template","annotationId":"303cccc1-c674-4d3a-8c89-19ea729fdbef","useHintRect":false,"description":"培育 行动页面 舞蹈课程按钮文字"},"cc8a495d-330d-447d-8a80-a8a6ecc409c5":{"name":"InPurodyuusu.TextActionVisual","displayName":"Vi. レッスン","type":"template","annotationId":"cc8a495d-330d-447d-8a80-a8a6ecc409c5","useHintRect":false,"description":"培育 行动页面 形象课程按钮文字"}},"annotations":[{"id":"be6836bd-ee42-432b-9166-469c74f32f0b","type":"rect","data":{"x1":11,"y1":8,"x2":237,"y2":196}},{"id":"d6b64759-26b7-45b1-bf8e-5c0d98611e0d","type":"rect","data":{"x1":116,"y1":1067,"x2":255,"y2":1097}},{"id":"303cccc1-c674-4d3a-8c89-19ea729fdbef","type":"rect","data":{"x1":288,"y1":1068,"x2":431,"y2":1096}},{"id":"cc8a495d-330d-447d-8a80-a8a6ecc409c5","type":"rect","data":{"x1":466,"y1":1067,"x2":606,"y2":1096}}]} \ No newline at end of file diff --git a/kotonebot/tasks/actions/in_purodyuusu.py b/kotonebot/tasks/actions/in_purodyuusu.py index f6c7d86..9f9b649 100644 --- a/kotonebot/tasks/actions/in_purodyuusu.py +++ b/kotonebot/tasks/actions/in_purodyuusu.py @@ -1,7 +1,7 @@ import math import time import logging -from typing_extensions import deprecated +from typing_extensions import deprecated, assert_never from itertools import cycle from typing import Generic, Iterable, Literal, NamedTuple, Callable, Generator, TypeVar, ParamSpec, cast @@ -12,7 +12,7 @@ from cv2.typing import MatLike from .. import R from . import loading -from ..common import conf +from ..common import ProduceAction, conf from .scenes import at_home from .commu import handle_unread_commu from kotonebot.errors import UnrecoverableError @@ -96,7 +96,7 @@ def handle_sp_lesson(): return False @action('执行推荐行动') -def handle_recommended_action(final_week: bool = False) -> ActionType: +def handle_recommended_action(final_week: bool = False) -> ProduceAction | None: """ 在行动选择页面,执行推荐行动 @@ -121,16 +121,20 @@ def handle_recommended_action(final_week: bool = False) -> ActionType: if result is None: logger.debug("No recommended lesson found") return None + recommended = None if not final_week: if result.index == 0: lesson_text = regex("Da") + recommended = ProduceAction.DANCE elif result.index == 1: lesson_text = regex("Vo|V0|VO") + recommended = ProduceAction.VOCAL elif result.index == 2: lesson_text = regex("Vi|V1|VI") + recommended = ProduceAction.VISUAL elif result.index == 3: rest() - return 'rest' + return ProduceAction.REST else: return None logger.info("Rec. lesson: %s", lesson_text) @@ -138,19 +142,22 @@ def handle_recommended_action(final_week: bool = False) -> ActionType: logger.debug("Try clicking lesson...") lesson_ret = ocr.expect(lesson_text) device.double_click(lesson_ret.rect) - return 'lesson' + return recommended else: if result.index == 0: template = R.InPurodyuusu.ButtonFinalPracticeDance + recommended = ProduceAction.DANCE elif result.index == 1: template = R.InPurodyuusu.ButtonFinalPracticeVocal + recommended = ProduceAction.VOCAL elif result.index == 2: template = R.InPurodyuusu.ButtonFinalPracticeVisual + recommended = ProduceAction.VISUAL else: return None logger.debug("Try clicking lesson...") device.double_click(image.expect_wait(template)) - return 'lesson' + return recommended class CardDetectResult(NamedTuple): type: int @@ -769,43 +776,128 @@ def produce_end(): sleep(1) logger.info("Produce completed.") +@action('执行行动') +def handle_action(action: ProduceAction, final_week: bool = False) -> ProduceAction | None: + """ + 执行行动 + + 前置条件:位于行动选择页面\n + 结束状态:若返回 True,取决于执行的行动。若返回 False,则仍然位于行动选择页面。 + + :param action: 行动类型 + :param final_week: 是否为冲刺周 + :return: 执行的行动 + """ + match action: + case ProduceAction.RECOMMENDED: + return handle_recommended_action(final_week) + case ProduceAction.DANCE: + # TODO: 这两个模板的名称要统一一下 + templ = R.InPurodyuusu.TextActionVisual if not final_week else R.InPurodyuusu.ButtonFinalPracticeVisual + if button := image.find(templ): + device.double_click(button) + return ProduceAction.DANCE + else: + return None + case ProduceAction.VOCAL: + templ = R.InPurodyuusu.TextActionVocal if not final_week else R.InPurodyuusu.ButtonFinalPracticeVocal + if button := image.find(templ): + device.double_click(button) + return ProduceAction.VOCAL + else: + return None + case ProduceAction.VISUAL: + templ = R.InPurodyuusu.TextActionDance if not final_week else R.InPurodyuusu.ButtonFinalPracticeDance + if button := image.find(templ): + device.double_click(button) + return ProduceAction.VISUAL + else: + return None + case ProduceAction.REST: + if is_rest_available(): + rest() + return ProduceAction.REST + case ProduceAction.OUTING: + if outing_available(): + enter_outing() + return ProduceAction.OUTING + case ProduceAction.STUDY: + if study_available(): + enter_study() + return ProduceAction.STUDY + case ProduceAction.ALLOWANCE: + if allowance_available(): + enter_allowance() + return ProduceAction.ALLOWANCE + case _: + logger.warning("Unknown action: %s", action) + return None + def week_normal(week_first: bool = False): until_action_scene(week_first) + logger.info("Handling actions...") + action: ProduceAction | None = None # SP 课程 if ( conf().produce.prefer_lesson_ap and handle_sp_lesson() ): - executed_action = 'lesson' + action = ProduceAction.DANCE else: - executed_action = handle_recommended_action() - logger.info("Executed recommended action: %s", executed_action) - # 推荐练习 - if executed_action == 'lesson': - until_practice_scene() - practice() - # 推荐休息 - elif executed_action == 'rest': - pass - # 没有推荐行动 - elif executed_action is None: - if outing_available(): - enter_outing() - elif study_available(): - enter_study() - elif allowance_available(): - enter_allowance() - elif is_rest_available(): - rest() - else: - raise ValueError("No action available.") + actions = conf().produce.actions_order + for action in actions: + logger.debug("Checking action: %s", action) + if action := handle_action(action): + logger.info("Action %s hit.", action) + break + match action: + case ( + ProduceAction.REST | + ProduceAction.OUTING | ProduceAction.STUDY | ProduceAction.ALLOWANCE + ): + # 什么都不需要做 + pass + case ProduceAction.DANCE | ProduceAction.VOCAL | ProduceAction.VISUAL: + until_practice_scene() + practice() + case ProduceAction.RECOMMENDED: + # RECOMMENDED 应当被 handle_recommended_action 转换为具体的行动 + raise ValueError("Recommended action should not be handled here.") + case None: + raise ValueError("Action is None.") + case _: + assert_never(action) until_action_scene() def week_final_lesson(): until_action_scene() - if handle_recommended_action(final_week=True) != 'lesson': - raise ValueError("Failed to enter recommended action on final week.") - sleep(5) + # if handle_recommended_action(final_week=True) != 'lesson': + # raise ValueError("Failed to enter recommended action on final week.") + # sleep(5) + action: ProduceAction | None = None + actions = conf().produce.actions_order + for action in actions: + logger.debug("Checking action: %s", action) + if action := handle_action(action, True): + logger.info("Action %s hit.", action) + break + match action: + case ( + ProduceAction.REST | + ProduceAction.OUTING | ProduceAction.STUDY | ProduceAction.ALLOWANCE + ): + # 什么都不需要做 + pass + case ProduceAction.DANCE | ProduceAction.VOCAL | ProduceAction.VISUAL: + until_practice_scene() + practice() + case ProduceAction.RECOMMENDED: + # RECOMMENDED 应当被 handle_recommended_action 转换为具体的行动 + raise ValueError("Recommended action should not be handled here.") + case None: + raise ValueError("Action is None.") + case _: + assert_never(action) until_practice_scene() practice() @@ -1050,6 +1142,7 @@ if __name__ == '__main__': # practice() + week_mid_exam() # week_final_exam() # exam('final') # produce_end() @@ -1058,7 +1151,7 @@ if __name__ == '__main__': # hajime_pro(start_from=16) # exam('mid') stage = (detect_produce_scene()) - hajime_regular_from_stage(stage, 'pro') + hajime_regular_from_stage(stage, 'regular') # click_recommended_card(card_count=skill_card_count()) # exam('mid') diff --git a/kotonebot/tasks/common.py b/kotonebot/tasks/common.py index fed8c83..49bc3b5 100644 --- a/kotonebot/tasks/common.py +++ b/kotonebot/tasks/common.py @@ -1,6 +1,6 @@ -from importlib import resources import os -from typing import Literal, Dict +from importlib import resources +from typing import Literal, Dict, NamedTuple, Tuple, TypeVar, Generic from enum import IntEnum, Enum from pydantic import BaseModel, ConfigDict @@ -8,6 +8,11 @@ from pydantic import BaseModel, ConfigDict # TODO: from kotonebot import config (context) 会和 kotonebot.config 冲突 from kotonebot.backend.context import config +T = TypeVar('T') +class ConfigEnum(Enum): + def display(self) -> str: + return self.value[1] + class Priority(IntEnum): START_GAME = 1 DEFAULT = 0 @@ -302,6 +307,33 @@ class ContestConfig(ConfigBaseModel): enabled: bool = False """是否启用竞赛""" +class ProduceAction(Enum): + RECOMMENDED = 'recommended' + VISUAL = 'visual' + VOCAL = 'vocal' + DANCE = 'dance' + # VISUAL_SP = 'visual_sp' + # VOCAL_SP = 'vocal_sp' + # DANCE_SP = 'dance_sp' + OUTING = 'outing' + STUDY = 'study' + ALLOWANCE = 'allowance' + REST = 'rest' + + @property + def display_name(self): + MAP = { + ProduceAction.RECOMMENDED: '推荐行动', + ProduceAction.VISUAL: '形象课程', + ProduceAction.VOCAL: '声乐课程', + ProduceAction.DANCE: '舞蹈课程', + ProduceAction.OUTING: '外出(おでかけ)', + ProduceAction.STUDY: '自习(授業)', + ProduceAction.ALLOWANCE: '活动支给(活動支給)', + ProduceAction.REST: '休息', + } + return MAP[self] + class ProduceConfig(ConfigBaseModel): enabled: bool = False """是否启用培育""" @@ -340,6 +372,21 @@ class ProduceConfig(ConfigBaseModel): 启用后,若出现 SP 课程,则会优先执行 SP 课程,而不是推荐课程。 若出现多个 SP 课程,随机选择一个。 """ + actions_order: list[ProduceAction] = [ + ProduceAction.RECOMMENDED, + ProduceAction.VISUAL, + ProduceAction.VOCAL, + ProduceAction.DANCE, + ProduceAction.ALLOWANCE, + ProduceAction.OUTING, + ProduceAction.STUDY, + ProduceAction.REST, + ] + """ + 行动优先级 + + 每一周的行动将会按这里设置的优先级执行。 + """ class MissionRewardConfig(ConfigBaseModel): enabled: bool = False diff --git a/kotonebot/ui/gr.py b/kotonebot/ui/gr.py index 5a2aa7b..7ffda5c 100644 --- a/kotonebot/ui/gr.py +++ b/kotonebot/ui/gr.py @@ -16,7 +16,7 @@ from kotonebot.config.manager import load_config, save_config from kotonebot.tasks.common import ( BaseConfig, APShopItems, PurchaseConfig, ActivityFundsConfig, PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig, - MissionRewardConfig, PIdol, DailyMoneyShopItems + MissionRewardConfig, PIdol, DailyMoneyShopItems, ProduceAction ) from kotonebot.config.base_config import UserConfig, BackendConfig from kotonebot.backend.bot import KotoneBot @@ -255,6 +255,7 @@ class KotoneBotUI: follow_producer: bool, self_study_lesson: Literal['dance', 'visual', 'vocal'], prefer_lesson_ap: bool, + actions_order: List[str], mission_reward_enabled: bool, ) -> str: ap_items_enum: List[Literal[0, 1, 2, 3]] = [] @@ -311,7 +312,8 @@ class KotoneBotUI: use_note_boost=use_note_boost, follow_producer=follow_producer, self_study_lesson=self_study_lesson, - prefer_lesson_ap=prefer_lesson_ap + prefer_lesson_ap=prefer_lesson_ap, + actions_order=[ProduceAction(action) for action in actions_order] ), mission_reward=MissionRewardConfig( enabled=mission_reward_enabled @@ -585,7 +587,14 @@ class KotoneBotUI: value=self.current_config.options.produce.prefer_lesson_ap, info=ProduceConfig.model_fields['prefer_lesson_ap'].description ) - + actions_order = gr.Dropdown( + choices=[(action.display_name, action.value) for action in ProduceAction], + value=[action.value for action in self.current_config.options.produce.actions_order], + label="行动优先级", + info="设置每周行动的优先级顺序", + multiselect=True + ) + produce_enabled.change( fn=lambda x: gr.Group(visible=x), inputs=[produce_enabled], @@ -597,7 +606,7 @@ class KotoneBotUI: inputs=[auto_set_memory], outputs=[memory_sets_group] ) - return produce_enabled, produce_mode, produce_count, produce_idols, memory_sets, auto_set_memory, auto_set_support, use_pt_boost, use_note_boost, follow_producer, self_study_lesson, prefer_lesson_ap + return produce_enabled, produce_mode, produce_count, produce_idols, memory_sets, auto_set_memory, auto_set_support, use_pt_boost, use_note_boost, follow_producer, self_study_lesson, prefer_lesson_ap, actions_order def _create_settings_tab(self) -> None: with gr.Tab("设置"):