feat(task): 支持指定培育中行动优先级

This commit is contained in:
XcantloadX 2025-03-08 17:33:27 +08:00
parent d5c64dfdbc
commit fc8c456902
4 changed files with 188 additions and 39 deletions

View File

@ -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}}]}
{"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}}]}

View File

@ -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')

View File

@ -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

View File

@ -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("设置"):