feat(*): 日常新增支持指定购买商品 & 部分优化
1. 日常新增支持指定购买商品 2. 新增 DispatcherContext.expand,允许在一个 dispatcher 函数内复用其他 dispatcher 函数 3. 修复 make_resources.py 生成结果中部分变量命名格式不正确的问题
This commit is contained in:
parent
58a8a8da72
commit
4154c5541e
|
@ -115,7 +115,7 @@ def action(
|
||||||
pass_through: bool = False,
|
pass_through: bool = False,
|
||||||
priority: int = 0,
|
priority: int = 0,
|
||||||
screenshot_mode: ScreenshotMode | None = None,
|
screenshot_mode: ScreenshotMode | None = None,
|
||||||
dispatcher: Literal[True] = True,
|
dispatcher: Literal[True, 'fragment'] = True,
|
||||||
) -> Callable[[Callable[Concatenate[DispatcherContext, P], R]], Callable[P, R]]:
|
) -> Callable[[Callable[Concatenate[DispatcherContext, P], R]], Callable[P, R]]:
|
||||||
"""
|
"""
|
||||||
`action` 装饰器,用于标记一个函数为动作函数。
|
`action` 装饰器,用于标记一个函数为动作函数。
|
||||||
|
@ -135,6 +135,9 @@ def action(
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# TODO: 需要找个地方统一管理这些属性名
|
||||||
|
ATTR_ORIGINAL_FUNC = '_kb_inner'
|
||||||
|
ATTR_ACTION_MARK = '__kb_action_mark'
|
||||||
def action(*args, **kwargs):
|
def action(*args, **kwargs):
|
||||||
def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
|
def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
|
||||||
description = description or func.__doc__ or ''
|
description = description or func.__doc__ or ''
|
||||||
|
@ -143,7 +146,6 @@ def action(*args, **kwargs):
|
||||||
logger.debug(f'Action "{name}" registered.')
|
logger.debug(f'Action "{name}" registered.')
|
||||||
return action
|
return action
|
||||||
|
|
||||||
|
|
||||||
if len(args) == 1 and isinstance(args[0], Callable):
|
if len(args) == 1 and isinstance(args[0], Callable):
|
||||||
func = args[0]
|
func = args[0]
|
||||||
action = _register(_placeholder, func.__name__, func.__doc__)
|
action = _register(_placeholder, func.__name__, func.__doc__)
|
||||||
|
@ -154,6 +156,8 @@ def action(*args, **kwargs):
|
||||||
ContextStackVars.pop()
|
ContextStackVars.pop()
|
||||||
current_callstack.pop()
|
current_callstack.pop()
|
||||||
return ret
|
return ret
|
||||||
|
setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
|
||||||
|
setattr(_wrapper, ATTR_ACTION_MARK, True)
|
||||||
action.func = _wrapper
|
action.func = _wrapper
|
||||||
return _wrapper
|
return _wrapper
|
||||||
else:
|
else:
|
||||||
|
@ -163,7 +167,7 @@ def action(*args, **kwargs):
|
||||||
priority = kwargs.get('priority', 0)
|
priority = kwargs.get('priority', 0)
|
||||||
screenshot_mode = kwargs.get('screenshot_mode', None)
|
screenshot_mode = kwargs.get('screenshot_mode', None)
|
||||||
dispatcher = kwargs.get('dispatcher', False)
|
dispatcher = kwargs.get('dispatcher', False)
|
||||||
if dispatcher:
|
if dispatcher == True or dispatcher == 'fragment':
|
||||||
if not (screenshot_mode is None or screenshot_mode == 'manual'):
|
if not (screenshot_mode is None or screenshot_mode == 'manual'):
|
||||||
raise ValueError('`screenshot_mode` must be None or "manual" when `dispatcher=True`.')
|
raise ValueError('`screenshot_mode` must be None or "manual" when `dispatcher=True`.')
|
||||||
screenshot_mode = 'manual'
|
screenshot_mode = 'manual'
|
||||||
|
@ -175,7 +179,7 @@ def action(*args, **kwargs):
|
||||||
return func
|
return func
|
||||||
else:
|
else:
|
||||||
if dispatcher:
|
if dispatcher:
|
||||||
func = dispatcher_decorator(func) # type: ignore
|
func = dispatcher_decorator(func, fragment=(dispatcher == 'fragment')) # type: ignore
|
||||||
def _wrapper(*args: P.args, **kwargs: P.kwargs):
|
def _wrapper(*args: P.args, **kwargs: P.kwargs):
|
||||||
current_callstack.append(action)
|
current_callstack.append(action)
|
||||||
vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
|
vars = ContextStackVars.push(screenshot_mode=screenshot_mode)
|
||||||
|
@ -183,6 +187,8 @@ def action(*args, **kwargs):
|
||||||
ContextStackVars.pop()
|
ContextStackVars.pop()
|
||||||
current_callstack.pop()
|
current_callstack.pop()
|
||||||
return ret
|
return ret
|
||||||
|
setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
|
||||||
|
setattr(_wrapper, ATTR_ACTION_MARK, True)
|
||||||
action.func = _wrapper
|
action.func = _wrapper
|
||||||
return _wrapper
|
return _wrapper
|
||||||
return _action_decorator
|
return _action_decorator
|
||||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
import inspect
|
import inspect
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from types import CodeType
|
from types import CodeType
|
||||||
from typing import Any, Callable, Concatenate, TypeVar, ParamSpec, Literal, Protocol
|
from typing import Annotated, Any, Callable, Concatenate, TypeVar, ParamSpec, Literal, Protocol, cast
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@ -15,22 +15,75 @@ P = ParamSpec('P')
|
||||||
R = TypeVar('R')
|
R = TypeVar('R')
|
||||||
ThenAction = Literal['click', 'log']
|
ThenAction = Literal['click', 'log']
|
||||||
DoAction = Literal['click']
|
DoAction = Literal['click']
|
||||||
|
# TODO: 需要找个地方统一管理这些属性名
|
||||||
|
ATTR_DISPATCHER_MARK = '__kb_dispatcher_mark'
|
||||||
|
ATTR_ORIGINAL_FUNC = '_kb_inner'
|
||||||
|
|
||||||
|
|
||||||
|
class DispatchFunc: pass
|
||||||
|
|
||||||
|
wrapper_to_func: dict[Callable, Callable] = {}
|
||||||
|
|
||||||
class DispatcherContext:
|
class DispatcherContext:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.finished: bool = False
|
self.finished: bool = False
|
||||||
|
self._first_run: bool = True
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
|
"""标记已完成 dispatcher 循环。循环将在下次条件检测时退出。"""
|
||||||
self.finished = True
|
self.finished = True
|
||||||
|
|
||||||
def dispatcher(func: Callable[Concatenate[DispatcherContext, P], R]) -> Callable[P, R]:
|
def expand(self, func: Annotated[Callable[[], Any], DispatchFunc], ignore_finish: bool = True):
|
||||||
"""
|
"""
|
||||||
|
调用其他 dispatcher 函数。
|
||||||
|
|
||||||
|
使用 `expand` 和直接调用的区别是:
|
||||||
|
* 直接调用会执行 while 循环,直到满足结束条件
|
||||||
|
* 而使用 `expand` 则只会执行一次。效果类似于将目标函数里的代码直接复制粘贴过来。
|
||||||
|
"""
|
||||||
|
# 获取原始函数
|
||||||
|
original_func = func
|
||||||
|
while not getattr(original_func, ATTR_DISPATCHER_MARK, False):
|
||||||
|
original_func = getattr(original_func, ATTR_ORIGINAL_FUNC)
|
||||||
|
original_func = getattr(original_func, ATTR_ORIGINAL_FUNC)
|
||||||
|
|
||||||
|
if not original_func:
|
||||||
|
raise ValueError(f'{repr(func)} is not a dispatcher function.')
|
||||||
|
elif not callable(original_func):
|
||||||
|
raise ValueError(f'{repr(original_func)} is not callable.')
|
||||||
|
original_func = cast(Callable[[DispatcherContext], Any], original_func)
|
||||||
|
|
||||||
|
old_finished = self.finished
|
||||||
|
ret = original_func(self)
|
||||||
|
if ignore_finish:
|
||||||
|
self.finished = old_finished
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def beginning(self) -> bool:
|
||||||
|
"""是否为第一次运行"""
|
||||||
|
return self._first_run
|
||||||
|
|
||||||
|
@property
|
||||||
|
def finishing(self) -> bool:
|
||||||
|
"""是否即将结束运行"""
|
||||||
|
return self.finished
|
||||||
|
|
||||||
|
def dispatcher(
|
||||||
|
func: Callable[Concatenate[DispatcherContext, P], R],
|
||||||
|
*,
|
||||||
|
fragment: bool = False
|
||||||
|
) -> Annotated[Callable[P, R], DispatchFunc]:
|
||||||
|
"""
|
||||||
注意:\n
|
注意:\n
|
||||||
此装饰器必须在应用 @action/@task 装饰器后再应用,且 `screenshot_mode='manual'` 参数必须设置。
|
此装饰器必须在应用 @action/@task 装饰器后再应用,且 `screenshot_mode='manual'` 参数必须设置。
|
||||||
或者也可以使用 @action/@task 装饰器中的 `dispatcher=True` 参数,
|
或者也可以使用 @action/@task 装饰器中的 `dispatcher=True` 参数,
|
||||||
那么就没有上面两个要求了。
|
那么就没有上面两个要求了。
|
||||||
|
|
||||||
|
:param fragment:
|
||||||
|
片段模式,默认不启用。
|
||||||
|
启用后,被装饰函数将会只执行依次,
|
||||||
|
而不会一直循环到 ctx.finish() 被调用。
|
||||||
"""
|
"""
|
||||||
ctx = DispatcherContext()
|
ctx = DispatcherContext()
|
||||||
def wrapper(*args: P.args, **kwargs: P.kwargs):
|
def wrapper(*args: P.args, **kwargs: P.kwargs):
|
||||||
|
@ -38,5 +91,19 @@ def dispatcher(func: Callable[Concatenate[DispatcherContext, P], R]) -> Callable
|
||||||
from kotonebot import device
|
from kotonebot import device
|
||||||
device.update_screenshot()
|
device.update_screenshot()
|
||||||
ret = func(ctx, *args, **kwargs)
|
ret = func(ctx, *args, **kwargs)
|
||||||
|
ctx._first_run = False
|
||||||
return ret
|
return ret
|
||||||
return wrapper
|
def fragment_wrapper(*args: P.args, **kwargs: P.kwargs):
|
||||||
|
from kotonebot import device
|
||||||
|
device.update_screenshot()
|
||||||
|
return func(ctx, *args, **kwargs)
|
||||||
|
setattr(wrapper, ATTR_ORIGINAL_FUNC, func)
|
||||||
|
setattr(fragment_wrapper, ATTR_ORIGINAL_FUNC, func)
|
||||||
|
setattr(wrapper, ATTR_DISPATCHER_MARK, True)
|
||||||
|
setattr(fragment_wrapper, ATTR_DISPATCHER_MARK, True)
|
||||||
|
wrapper_to_func[wrapper] = func
|
||||||
|
if fragment:
|
||||||
|
return fragment_wrapper
|
||||||
|
|
||||||
|
else:
|
||||||
|
return wrapper
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Literal
|
from typing import Literal, Dict
|
||||||
from enum import IntEnum, Enum
|
from enum import IntEnum, Enum
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
@ -102,13 +102,136 @@ class PIdol(Enum):
|
||||||
藤田ことね_冠菊 = ["藤田ことね", "冠菊"]
|
藤田ことね_冠菊 = ["藤田ことね", "冠菊"]
|
||||||
藤田ことね_初声 = ["藤田ことね", "初声"]
|
藤田ことね_初声 = ["藤田ことね", "初声"]
|
||||||
藤田ことね_学園生活 = ["藤田ことね", "学園生活"]
|
藤田ことね_学園生活 = ["藤田ことね", "学園生活"]
|
||||||
|
|
||||||
|
class DailyMoneyShopItems(IntEnum):
|
||||||
|
"""日常商店物品"""
|
||||||
|
Recommendations = -1
|
||||||
|
"""所有推荐商品"""
|
||||||
|
LessonNote = 0
|
||||||
|
"""レッスンノート"""
|
||||||
|
VeteranNote = 1
|
||||||
|
"""ベテランノート"""
|
||||||
|
SupportEnhancementPt = 2
|
||||||
|
"""サポート強化Pt 支援强化Pt"""
|
||||||
|
SenseNoteVocal = 3
|
||||||
|
"""センスノート(ボーカル)感性笔记(声乐)"""
|
||||||
|
SenseNoteDance = 4
|
||||||
|
"""センスノート(ダンス)感性笔记(舞蹈)"""
|
||||||
|
SenseNoteVisual = 5
|
||||||
|
"""センスノート(ビジュアル)感性笔记(形象)"""
|
||||||
|
LogicNoteVocal = 6
|
||||||
|
"""ロジックノート(ボーカル)理性笔记(声乐)"""
|
||||||
|
LogicNoteDance = 7
|
||||||
|
"""ロジックノート(ダンス)理性笔记(舞蹈)"""
|
||||||
|
LogicNoteVisual = 8
|
||||||
|
"""ロジックノート(ビジュアル)理性笔记(形象)"""
|
||||||
|
AnomalyNoteVocal = 9
|
||||||
|
"""アノマリーノート(ボーカル)非凡笔记(声乐)"""
|
||||||
|
AnomalyNoteDance = 10
|
||||||
|
"""アノマリーノート(ダンス)非凡笔记(舞蹈)"""
|
||||||
|
AnomalyNoteVisual = 11
|
||||||
|
"""アノマリーノート(ビジュアル)非凡笔记(形象)"""
|
||||||
|
RechallengeTicket = 12
|
||||||
|
"""再挑戦チケット 重新挑战券"""
|
||||||
|
RecordKey = 13
|
||||||
|
|
||||||
|
# 碎片
|
||||||
|
"""記録の鍵 解锁交流的物品"""
|
||||||
|
IdolPiece_倉本千奈_WonderScale = 14
|
||||||
|
"""倉本千奈 WonderScale 碎片"""
|
||||||
|
IdolPiece_篠泽广_光景 = 15
|
||||||
|
"""篠泽广 光景 碎片"""
|
||||||
|
IdolPiece_紫云清夏_TameLieOneStep = 16
|
||||||
|
"""紫云清夏 Tame-Lie-One-Step 碎片"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_ui_text(cls, item: "DailyMoneyShopItems") -> str:
|
||||||
|
"""获取枚举值对应的UI显示文本"""
|
||||||
|
MAP = {
|
||||||
|
cls.Recommendations: "所有推荐商品",
|
||||||
|
cls.LessonNote: "课程笔记",
|
||||||
|
cls.VeteranNote: "老手笔记",
|
||||||
|
cls.SupportEnhancementPt: "支援强化点数",
|
||||||
|
cls.SenseNoteVocal: "感性笔记(声乐)",
|
||||||
|
cls.SenseNoteDance: "感性笔记(舞蹈)",
|
||||||
|
cls.SenseNoteVisual: "感性笔记(形象)",
|
||||||
|
cls.LogicNoteVocal: "理性笔记(声乐)",
|
||||||
|
cls.LogicNoteDance: "理性笔记(舞蹈)",
|
||||||
|
cls.LogicNoteVisual: "理性笔记(形象)",
|
||||||
|
cls.AnomalyNoteVocal: "非凡笔记(声乐)",
|
||||||
|
cls.AnomalyNoteDance: "非凡笔记(舞蹈)",
|
||||||
|
cls.AnomalyNoteVisual: "非凡笔记(形象)",
|
||||||
|
cls.RechallengeTicket: "重新挑战券",
|
||||||
|
cls.RecordKey: "记录钥匙",
|
||||||
|
cls.IdolPiece_倉本千奈_WonderScale: "倉本千奈 WonderScale 碎片",
|
||||||
|
cls.IdolPiece_篠泽广_光景: "篠泽广 光景 碎片",
|
||||||
|
cls.IdolPiece_紫云清夏_TameLieOneStep: "紫云清夏 Tame-Lie-One-Step 碎片"
|
||||||
|
}
|
||||||
|
return MAP.get(item, str(item))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls) -> list[tuple[str, 'DailyMoneyShopItems']]:
|
||||||
|
"""获取所有枚举值及其对应的UI显示文本"""
|
||||||
|
return [(cls.to_ui_text(item), item) for item in cls]
|
||||||
|
|
||||||
|
def to_resource(self):
|
||||||
|
from . import R
|
||||||
|
match self:
|
||||||
|
case DailyMoneyShopItems.Recommendations:
|
||||||
|
return R.Daily.TextShopRecommended
|
||||||
|
case DailyMoneyShopItems.LessonNote:
|
||||||
|
return R.Shop.ItemLessonNote
|
||||||
|
case DailyMoneyShopItems.VeteranNote:
|
||||||
|
return R.Shop.ItemVeteranNote
|
||||||
|
case DailyMoneyShopItems.SupportEnhancementPt:
|
||||||
|
return R.Shop.ItemSupportEnhancementPt
|
||||||
|
case DailyMoneyShopItems.SenseNoteVocal:
|
||||||
|
return R.Shop.ItemSenseNoteVocal
|
||||||
|
case DailyMoneyShopItems.SenseNoteDance:
|
||||||
|
return R.Shop.ItemSenseNoteDance
|
||||||
|
case DailyMoneyShopItems.SenseNoteVisual:
|
||||||
|
return R.Shop.ItemSenseNoteVisual
|
||||||
|
case DailyMoneyShopItems.LogicNoteVocal:
|
||||||
|
return R.Shop.ItemLogicNoteVocal
|
||||||
|
case DailyMoneyShopItems.LogicNoteDance:
|
||||||
|
return R.Shop.ItemLogicNoteDance
|
||||||
|
case DailyMoneyShopItems.LogicNoteVisual:
|
||||||
|
return R.Shop.ItemLogicNoteVisual
|
||||||
|
case DailyMoneyShopItems.AnomalyNoteVocal:
|
||||||
|
return R.Shop.ItemAnomalyNoteVocal
|
||||||
|
case DailyMoneyShopItems.AnomalyNoteDance:
|
||||||
|
return R.Shop.ItemAnomalyNoteDance
|
||||||
|
case DailyMoneyShopItems.AnomalyNoteVisual:
|
||||||
|
return R.Shop.ItemAnomalyNoteVisual
|
||||||
|
case DailyMoneyShopItems.RechallengeTicket:
|
||||||
|
return R.Shop.ItemRechallengeTicket
|
||||||
|
case DailyMoneyShopItems.RecordKey:
|
||||||
|
return R.Shop.ItemRecordKey
|
||||||
|
case DailyMoneyShopItems.IdolPiece_倉本千奈_WonderScale:
|
||||||
|
return R.Shop.IdolPiece.倉本千奈_WonderScale
|
||||||
|
case DailyMoneyShopItems.IdolPiece_篠泽广_光景:
|
||||||
|
return R.Shop.IdolPiece.篠泽广_光景
|
||||||
|
case DailyMoneyShopItems.IdolPiece_紫云清夏_TameLieOneStep:
|
||||||
|
return R.Shop.IdolPiece.紫云清夏_TameLieOneStep
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Unknown daily shop item: {self}")
|
||||||
|
|
||||||
|
|
||||||
class PurchaseConfig(BaseModel):
|
class PurchaseConfig(BaseModel):
|
||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
"""是否启用商店购买"""
|
"""是否启用商店购买"""
|
||||||
money_enabled: bool = False
|
money_enabled: bool = False
|
||||||
"""是否启用金币购买"""
|
"""是否启用金币购买"""
|
||||||
|
money_items: list[DailyMoneyShopItems] = []
|
||||||
|
"""金币商店要购买的物品"""
|
||||||
|
money_refresh_on: Literal['never', 'not_found', 'always'] = 'never'
|
||||||
|
"""
|
||||||
|
金币商店刷新逻辑。
|
||||||
|
|
||||||
|
* never: 从不刷新。
|
||||||
|
* not_found: 仅当要购买的物品不存在时刷新。
|
||||||
|
* always: 总是刷新。
|
||||||
|
"""
|
||||||
ap_enabled: bool = False
|
ap_enabled: bool = False
|
||||||
"""是否启用AP购买"""
|
"""是否启用AP购买"""
|
||||||
ap_items: list[Literal[0, 1, 2, 3]] = []
|
ap_items: list[Literal[0, 1, 2, 3]] = []
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from . import R
|
from . import R
|
||||||
from .common import conf, PIdol
|
from .common import conf, PIdol
|
||||||
|
@ -95,6 +96,9 @@ def select_idol(target_titles: list[str] | PIdol):
|
||||||
def do_produce(idol: PIdol | None = None):
|
def do_produce(idol: PIdol | None = None):
|
||||||
"""
|
"""
|
||||||
进行培育流程
|
进行培育流程
|
||||||
|
|
||||||
|
前置条件:可导航至首页的任意页面\n
|
||||||
|
结束状态:游戏首页\n
|
||||||
|
|
||||||
:param idol: 要培育的偶像。如果为 None,则使用配置文件中的偶像。
|
:param idol: 要培育的偶像。如果为 None,则使用配置文件中的偶像。
|
||||||
"""
|
"""
|
||||||
|
@ -162,14 +166,23 @@ def do_produce(idol: PIdol | None = None):
|
||||||
wait_loading_end()
|
wait_loading_end()
|
||||||
hajime_regular()
|
hajime_regular()
|
||||||
|
|
||||||
|
|
||||||
@task('培育')
|
@task('培育')
|
||||||
def produce_task():
|
def produce_task(count: Optional[int] = None):
|
||||||
|
"""
|
||||||
|
培育任务
|
||||||
|
|
||||||
|
:param count:
|
||||||
|
培育次数。若为 None,则从配置文件中读入。
|
||||||
|
"""
|
||||||
import time
|
import time
|
||||||
start_time = time.time()
|
if count is None:
|
||||||
do_produce()
|
count = conf().produce.produce_count
|
||||||
end_time = time.time()
|
for _ in range(count):
|
||||||
logger.info(f"Produce time used: {format_time(end_time - start_time)}")
|
start_time = time.time()
|
||||||
|
do_produce()
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
logger.info(f"Produce time used: {format_time(end_time - start_time)}")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -1,14 +1,104 @@
|
||||||
"""从商店购买物品"""
|
"""从商店购买物品"""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
from . import R
|
from . import R
|
||||||
from .common import conf
|
from .common import conf, DailyMoneyShopItems
|
||||||
from kotonebot.backend.util import cropped
|
from kotonebot.backend.util import cropped
|
||||||
from kotonebot import task, device, image, ocr, action, sleep
|
from kotonebot import task, device, image, ocr, action, sleep
|
||||||
|
from kotonebot.backend.dispatch import DispatcherContext, dispatcher
|
||||||
from .actions.scenes import goto_home, goto_shop, at_daily_shop
|
from .actions.scenes import goto_home, goto_shop, at_daily_shop
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@action('购买 Money 物品', screenshot_mode='manual-inherit')
|
||||||
|
def money_items2(items: Optional[list[DailyMoneyShopItems]] = None):
|
||||||
|
"""
|
||||||
|
购买 Money 物品
|
||||||
|
|
||||||
|
前置条件:商店页面的 マニー Tab\n
|
||||||
|
结束状态:-
|
||||||
|
|
||||||
|
:param items: 要购买的物品列表,默认为 None。为 None 时使用配置文件里的设置。
|
||||||
|
"""
|
||||||
|
# 前置条件:[screenshots\shop\money1.png]
|
||||||
|
logger.info(f'Purchasing マニー items.')
|
||||||
|
|
||||||
|
if items is None:
|
||||||
|
items = conf().purchase.money_items
|
||||||
|
|
||||||
|
device.update_screenshot()
|
||||||
|
if DailyMoneyShopItems.Recommendations in items:
|
||||||
|
dispatch_recommended_items()
|
||||||
|
items.remove(DailyMoneyShopItems.Recommendations)
|
||||||
|
|
||||||
|
finished = []
|
||||||
|
max_scroll = 3
|
||||||
|
scroll = 0
|
||||||
|
while items:
|
||||||
|
for item in items:
|
||||||
|
if image.find(item.to_resource()):
|
||||||
|
logger.info(f'Purchasing {item.to_ui_text(item)}...')
|
||||||
|
device.click()
|
||||||
|
dispatch_purchase_dialog()
|
||||||
|
finished.append(item)
|
||||||
|
items = [item for item in items if item not in finished]
|
||||||
|
# 全都买完了
|
||||||
|
if not items:
|
||||||
|
break
|
||||||
|
# 还有,翻页后继续
|
||||||
|
else:
|
||||||
|
device.swipe_scaled(x1=0.5, x2=0.5, y1=0.8, y2=0.5)
|
||||||
|
sleep(0.5)
|
||||||
|
device.update_screenshot()
|
||||||
|
scroll += 1
|
||||||
|
if scroll >= max_scroll:
|
||||||
|
break
|
||||||
|
logger.info(f'Purchasing money items completed. {len(finished)} item(s) purchased.')
|
||||||
|
if items:
|
||||||
|
logger.info(f'{len(items)} item(s) not purchased: {", ".join([item.to_ui_text(item) for item in items])}')
|
||||||
|
|
||||||
|
@action('购买推荐商品', dispatcher=True)
|
||||||
|
def dispatch_recommended_items(ctx: DispatcherContext):
|
||||||
|
"""
|
||||||
|
购买推荐商品
|
||||||
|
|
||||||
|
前置条件:商店页面的 マニー Tab\n
|
||||||
|
结束状态:-
|
||||||
|
"""
|
||||||
|
# 前置条件:[screenshots\shop\money1.png]
|
||||||
|
if ctx.beginning:
|
||||||
|
logger.info(f'Start purchasing recommended items.')
|
||||||
|
|
||||||
|
if image.find(R.Daily.TextShopRecommended):
|
||||||
|
logger.info(f'Clicking on recommended item.') # TODO: 计数
|
||||||
|
device.click()
|
||||||
|
elif ctx.expand(dispatch_purchase_dialog):
|
||||||
|
pass
|
||||||
|
elif image.find(R.Daily.IconTitleDailyShop) and not image.find(R.Daily.TextShopRecommended):
|
||||||
|
logger.info(f'No recommended item found. Finished.')
|
||||||
|
ctx.finish()
|
||||||
|
|
||||||
|
@action('确认购买', dispatcher=True)
|
||||||
|
def dispatch_purchase_dialog(ctx: DispatcherContext):
|
||||||
|
"""
|
||||||
|
确认购买
|
||||||
|
|
||||||
|
前置条件:购买确认对话框\n
|
||||||
|
结束状态:对话框关闭后原来的界面
|
||||||
|
"""
|
||||||
|
# 前置条件:[screenshots\shop\dialog.png]
|
||||||
|
if image.find(R.Daily.ButtonShopCountAdd, colored=True):
|
||||||
|
logger.debug('Adjusting quantity(+1)...')
|
||||||
|
device.click()
|
||||||
|
elif image.find(R.Common.ButtonConfirm):
|
||||||
|
logger.debug('Confirming purchase...')
|
||||||
|
# device.click()
|
||||||
|
device.click(image.expect(R.InPurodyuusu.ButtonCancel))
|
||||||
|
ctx.finish()
|
||||||
|
|
||||||
|
@deprecated('改用 `money_items2`')
|
||||||
@action('购买 Money 物品')
|
@action('购买 Money 物品')
|
||||||
def money_items():
|
def money_items():
|
||||||
"""
|
"""
|
||||||
|
@ -99,7 +189,7 @@ def purchase():
|
||||||
# 购买マニー物品
|
# 购买マニー物品
|
||||||
if conf().purchase.money_enabled:
|
if conf().purchase.money_enabled:
|
||||||
image.expect_wait(R.Daily.IconShopMoney)
|
image.expect_wait(R.Daily.IconShopMoney)
|
||||||
money_items()
|
money_items2()
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
else:
|
else:
|
||||||
logger.info('Money purchase is disabled.')
|
logger.info('Money purchase is disabled.')
|
||||||
|
@ -121,6 +211,4 @@ if __name__ == '__main__':
|
||||||
import logging
|
import logging
|
||||||
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
# money_items()
|
dispatch_recommended_items()
|
||||||
# ap_items([0, 1, 3])
|
|
||||||
purchase()
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from kotonebot.config.manager import load_config, save_config
|
||||||
from kotonebot.tasks.common import (
|
from kotonebot.tasks.common import (
|
||||||
BaseConfig, APShopItems, PurchaseConfig, ActivityFundsConfig,
|
BaseConfig, APShopItems, PurchaseConfig, ActivityFundsConfig,
|
||||||
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
|
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
|
||||||
MissionRewardConfig, PIdol
|
MissionRewardConfig, PIdol, DailyMoneyShopItems
|
||||||
)
|
)
|
||||||
from kotonebot.config.base_config import UserConfig, BackendConfig
|
from kotonebot.config.base_config import UserConfig, BackendConfig
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ class KotoneBotUI:
|
||||||
money_enabled: bool,
|
money_enabled: bool,
|
||||||
ap_enabled: bool,
|
ap_enabled: bool,
|
||||||
ap_items: List[str],
|
ap_items: List[str],
|
||||||
|
money_items: List[DailyMoneyShopItems],
|
||||||
activity_funds_enabled: bool,
|
activity_funds_enabled: bool,
|
||||||
presents_enabled: bool,
|
presents_enabled: bool,
|
||||||
assignment_enabled: bool,
|
assignment_enabled: bool,
|
||||||
|
@ -125,9 +126,9 @@ class KotoneBotUI:
|
||||||
) -> str:
|
) -> str:
|
||||||
ap_items_enum: List[Literal[0, 1, 2, 3]] = []
|
ap_items_enum: List[Literal[0, 1, 2, 3]] = []
|
||||||
ap_items_map: Dict[str, APShopItems] = {
|
ap_items_map: Dict[str, APShopItems] = {
|
||||||
"获取支援强化 Pt 提升": APShopItems.PRODUCE_PT_UP,
|
"支援强化点数提升": APShopItems.PRODUCE_PT_UP,
|
||||||
"获取笔记数提升": APShopItems.PRODUCE_NOTE_UP,
|
"笔记数提升": APShopItems.PRODUCE_NOTE_UP,
|
||||||
"再挑战券": APShopItems.RECHALLENGE,
|
"重新挑战券": APShopItems.RECHALLENGE,
|
||||||
"回忆再生成券": APShopItems.REGENERATE_MEMORY
|
"回忆再生成券": APShopItems.REGENERATE_MEMORY
|
||||||
}
|
}
|
||||||
for item in ap_items:
|
for item in ap_items:
|
||||||
|
@ -141,6 +142,7 @@ class KotoneBotUI:
|
||||||
purchase=PurchaseConfig(
|
purchase=PurchaseConfig(
|
||||||
enabled=purchase_enabled,
|
enabled=purchase_enabled,
|
||||||
money_enabled=money_enabled,
|
money_enabled=money_enabled,
|
||||||
|
money_items=money_items,
|
||||||
ap_enabled=ap_enabled,
|
ap_enabled=ap_enabled,
|
||||||
ap_items=ap_items_enum
|
ap_items=ap_items_enum
|
||||||
),
|
),
|
||||||
|
@ -217,7 +219,7 @@ class KotoneBotUI:
|
||||||
outputs=[task_status]
|
outputs=[task_status]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_purchase_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Dropdown]:
|
def _create_purchase_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Dropdown]:
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
gr.Markdown("### 商店购买设置")
|
gr.Markdown("### 商店购买设置")
|
||||||
purchase_enabled = gr.Checkbox(
|
purchase_enabled = gr.Checkbox(
|
||||||
|
@ -229,6 +231,15 @@ class KotoneBotUI:
|
||||||
label="启用金币购买",
|
label="启用金币购买",
|
||||||
value=self.current_config.options.purchase.money_enabled
|
value=self.current_config.options.purchase.money_enabled
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 添加金币商店商品选择
|
||||||
|
money_items = gr.Dropdown(
|
||||||
|
multiselect=True,
|
||||||
|
choices=list(DailyMoneyShopItems.all()),
|
||||||
|
value=self.current_config.options.purchase.money_items,
|
||||||
|
label="金币商店购买物品"
|
||||||
|
)
|
||||||
|
|
||||||
ap_enabled = gr.Checkbox(
|
ap_enabled = gr.Checkbox(
|
||||||
label="启用AP购买",
|
label="启用AP购买",
|
||||||
value=self.current_config.options.purchase.ap_enabled
|
value=self.current_config.options.purchase.ap_enabled
|
||||||
|
@ -237,9 +248,9 @@ class KotoneBotUI:
|
||||||
# 转换枚举值为显示文本
|
# 转换枚举值为显示文本
|
||||||
selected_items: List[str] = []
|
selected_items: List[str] = []
|
||||||
ap_items_map = {
|
ap_items_map = {
|
||||||
APShopItems.PRODUCE_PT_UP: "获取支援强化 Pt 提升",
|
APShopItems.PRODUCE_PT_UP: "支援强化点数提升",
|
||||||
APShopItems.PRODUCE_NOTE_UP: "获取笔记数提升",
|
APShopItems.PRODUCE_NOTE_UP: "笔记数提升",
|
||||||
APShopItems.RECHALLENGE: "再挑战券",
|
APShopItems.RECHALLENGE: "重新挑战券",
|
||||||
APShopItems.REGENERATE_MEMORY: "回忆再生成券"
|
APShopItems.REGENERATE_MEMORY: "回忆再生成券"
|
||||||
}
|
}
|
||||||
for item_value in self.current_config.options.purchase.ap_items:
|
for item_value in self.current_config.options.purchase.ap_items:
|
||||||
|
@ -259,7 +270,7 @@ class KotoneBotUI:
|
||||||
inputs=[purchase_enabled],
|
inputs=[purchase_enabled],
|
||||||
outputs=[purchase_group]
|
outputs=[purchase_group]
|
||||||
)
|
)
|
||||||
return purchase_enabled, money_enabled, ap_enabled, ap_items
|
return purchase_enabled, money_enabled, ap_enabled, ap_items, money_items
|
||||||
|
|
||||||
def _create_work_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Checkbox, gr.Dropdown]:
|
def _create_work_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Checkbox, gr.Dropdown]:
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 861 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"definitions":{"0949c622-9067-4f0d-bac2-3f938a1d2ed2":{"name":"Shop.ItemLessonNote","displayName":"レッスンノート","type":"template","annotationId":"0949c622-9067-4f0d-bac2-3f938a1d2ed2","useHintRect":false},"b2af59e9-60e3-4d97-8c72-c7ba092113a3":{"name":"Shop.ItemVeteranNote","displayName":"ベテランノート","type":"template","annotationId":"b2af59e9-60e3-4d97-8c72-c7ba092113a3","useHintRect":false},"835489e2-b29b-426c-b4c9-3bb9f8eb6195":{"name":"Shop.ItemSupportEnhancementPt","displayName":"サポート強化Pt 支援强化Pt","type":"template","annotationId":"835489e2-b29b-426c-b4c9-3bb9f8eb6195","useHintRect":false},"c5b7d67e-7260-4f08-a0e9-4d31ce9bbecf":{"name":"Shop.ItemSenseNoteVocal","displayName":"センスノート(ボーカル)感性笔记(声乐)","type":"template","annotationId":"c5b7d67e-7260-4f08-a0e9-4d31ce9bbecf","useHintRect":false},"0f7d581d-cea3-4039-9205-732e4cd29293":{"name":"Shop.ItemSenseNoteDance","displayName":"センスノート(ダンス)感性笔记(舞蹈)","type":"template","annotationId":"0f7d581d-cea3-4039-9205-732e4cd29293","useHintRect":false},"d3cc3323-51af-4882-ae12-49e7384b746d":{"name":"Shop.ItemSenseNoteVisual","displayName":"センスノート(ビジュアル)感性笔记(形象)","type":"template","annotationId":"d3cc3323-51af-4882-ae12-49e7384b746d","useHintRect":false},"a1df3af1-a3e7-4521-a085-e4dc3cd1cc57":{"name":"Shop.ItemLogicNoteVocal","displayName":"ロジックノート(ボーカル)理性笔记(声乐)","type":"template","annotationId":"a1df3af1-a3e7-4521-a085-e4dc3cd1cc57","useHintRect":false},"a9fcaf04-0c1f-4b0f-bb5b-ede9da96180f":{"name":"Shop.ItemLogicNoteDance","displayName":"ロジックノート(ダンス)理性笔记(舞蹈)","type":"template","annotationId":"a9fcaf04-0c1f-4b0f-bb5b-ede9da96180f","useHintRect":false},"c3f536d6-a04a-4651-b3f9-dd2c22593f7f":{"name":"Shop.ItemLogicNoteVisual","displayName":"ロジックノート(ビジュアル)理性笔记(形象)","type":"template","annotationId":"c3f536d6-a04a-4651-b3f9-dd2c22593f7f","useHintRect":false},"eef25cf9-afd0-43b1-b9c5-fbd997bd5fe0":{"name":"Shop.ItemAnomalyNoteVocal","displayName":"アノマリーノート(ボーカル)非凡笔记(声乐)","type":"template","annotationId":"eef25cf9-afd0-43b1-b9c5-fbd997bd5fe0","useHintRect":false},"df991b42-ed8e-4f2c-bf0c-aa7522f147b6":{"name":"Shop.ItemAnomalyNoteDance","displayName":"アノマリーノート(ダンス)非凡笔记(舞蹈)","type":"template","annotationId":"df991b42-ed8e-4f2c-bf0c-aa7522f147b6","useHintRect":false}},"annotations":[{"id":"0949c622-9067-4f0d-bac2-3f938a1d2ed2","type":"rect","data":{"x1":243,"y1":355,"x2":313,"y2":441}},{"id":"b2af59e9-60e3-4d97-8c72-c7ba092113a3","type":"rect","data":{"x1":414,"y1":355,"x2":484,"y2":441}},{"id":"835489e2-b29b-426c-b4c9-3bb9f8eb6195","type":"rect","data":{"x1":574,"y1":363,"x2":662,"y2":438}},{"id":"c5b7d67e-7260-4f08-a0e9-4d31ce9bbecf","type":"rect","data":{"x1":71,"y1":594,"x2":142,"y2":667}},{"id":"0f7d581d-cea3-4039-9205-732e4cd29293","type":"rect","data":{"x1":241,"y1":593,"x2":309,"y2":667}},{"id":"d3cc3323-51af-4882-ae12-49e7384b746d","type":"rect","data":{"x1":417,"y1":586,"x2":481,"y2":668}},{"id":"a1df3af1-a3e7-4521-a085-e4dc3cd1cc57","type":"rect","data":{"x1":585,"y1":591,"x2":651,"y2":669}},{"id":"a9fcaf04-0c1f-4b0f-bb5b-ede9da96180f","type":"rect","data":{"x1":69,"y1":825,"x2":138,"y2":899}},{"id":"c3f536d6-a04a-4651-b3f9-dd2c22593f7f","type":"rect","data":{"x1":242,"y1":820,"x2":310,"y2":898}},{"id":"eef25cf9-afd0-43b1-b9c5-fbd997bd5fe0","type":"rect","data":{"x1":413,"y1":821,"x2":481,"y2":897}},{"id":"df991b42-ed8e-4f2c-bf0c-aa7522f147b6","type":"rect","data":{"x1":583,"y1":823,"x2":649,"y2":900}}]}
|
Binary file not shown.
After Width: | Height: | Size: 743 KiB |
|
@ -0,0 +1 @@
|
||||||
|
{"definitions":{"9340b854-025c-40da-9387-385d38433bef":{"name":"Shop.ItemAnomalyNoteVisual","displayName":"アノマリーノート(ビジュアル)非凡笔记(形象)","type":"template","annotationId":"9340b854-025c-40da-9387-385d38433bef","useHintRect":false},"ea1ba124-9cb3-4427-969a-bacd47e7d920":{"name":"Shop.ItemRechallengeTicket","displayName":"再挑戦チケット 重新挑战券","type":"template","annotationId":"ea1ba124-9cb3-4427-969a-bacd47e7d920","useHintRect":false},"1926f2f9-4bd7-48eb-9eba-28ec4efb0606":{"name":"Shop.ItemRecordKey","displayName":"記録の鍵 解锁交流的物品","type":"template","annotationId":"1926f2f9-4bd7-48eb-9eba-28ec4efb0606","useHintRect":false},"6720b6e8-ae80-4cc0-a885-518efe12b707":{"name":"Shop.IdolPiece.倉本千奈_WonderScale","displayName":"倉本千奈 WonderScale 碎片","type":"template","annotationId":"6720b6e8-ae80-4cc0-a885-518efe12b707","useHintRect":false},"afa06fdc-a345-4384-b25d-b16540830256":{"name":"Shop.IdolPiece.篠泽广_光景","displayName":"篠泽广 光景 碎片","type":"template","annotationId":"afa06fdc-a345-4384-b25d-b16540830256","useHintRect":false},"278b7d9c-707e-4392-9677-74574b5cdf42":{"name":"Shop.IdolPiece.紫云清夏_TameLieOneStep","displayName":"紫云清夏 Tame-Lie-One-Step 碎片","type":"template","annotationId":"278b7d9c-707e-4392-9677-74574b5cdf42","useHintRect":false},"e9ee330d-dfca-440e-8b8c-0a3b4e8c8730":{"name":"Daily.IconTitleDailyShop","displayName":"日常商店标题图标","type":"template","annotationId":"e9ee330d-dfca-440e-8b8c-0a3b4e8c8730","useHintRect":false}},"annotations":[{"id":"9340b854-025c-40da-9387-385d38433bef","type":"rect","data":{"x1":72,"y1":611,"x2":138,"y2":693}},{"id":"ea1ba124-9cb3-4427-969a-bacd47e7d920","type":"rect","data":{"x1":227,"y1":639,"x2":316,"y2":674}},{"id":"1926f2f9-4bd7-48eb-9eba-28ec4efb0606","type":"rect","data":{"x1":385,"y1":591,"x2":508,"y2":694}},{"id":"6720b6e8-ae80-4cc0-a885-518efe12b707","type":"rect","data":{"x1":589,"y1":633,"x2":638,"y2":678}},{"id":"afa06fdc-a345-4384-b25d-b16540830256","type":"rect","data":{"x1":83,"y1":867,"x2":134,"y2":912}},{"id":"278b7d9c-707e-4392-9677-74574b5cdf42","type":"rect","data":{"x1":247,"y1":864,"x2":301,"y2":907}},{"id":"e9ee330d-dfca-440e-8b8c-0a3b4e8c8730","type":"rect","data":{"x1":17,"y1":35,"x2":59,"y2":76}}]}
|
|
@ -180,7 +180,7 @@ def load_metadata(root_path: str, png_file: str) -> list[Resource]:
|
||||||
uuid=definition.annotationId,
|
uuid=definition.annotationId,
|
||||||
name=definition.name.split('.')[-1],
|
name=definition.name.split('.')[-1],
|
||||||
display_name=definition.displayName,
|
display_name=definition.displayName,
|
||||||
class_path=to_camel_cases(definition.name.split('.')[:-1]),
|
class_path=definition.name.split('.')[:-1],
|
||||||
|
|
||||||
rel_path=png_file,
|
rel_path=png_file,
|
||||||
abs_path=os.path.abspath(clips[definition.annotationId]),
|
abs_path=os.path.abspath(clips[definition.annotationId]),
|
||||||
|
@ -192,7 +192,7 @@ def load_metadata(root_path: str, png_file: str) -> list[Resource]:
|
||||||
hb = HintBox(
|
hb = HintBox(
|
||||||
name=definition.name.split('.')[-1],
|
name=definition.name.split('.')[-1],
|
||||||
display_name=definition.displayName,
|
display_name=definition.displayName,
|
||||||
class_path=to_camel_cases(definition.name.split('.')[:-1]),
|
class_path=definition.name.split('.')[:-1],
|
||||||
x1=annotation.data.x1,
|
x1=annotation.data.x1,
|
||||||
y1=annotation.data.y1,
|
y1=annotation.data.y1,
|
||||||
x2=annotation.data.x2,
|
x2=annotation.data.x2,
|
||||||
|
|
Loading…
Reference in New Issue