diff --git a/experiments/imgui_test.py b/experiments/imgui_test.py
deleted file mode 100644
index 6ab88c1..0000000
--- a/experiments/imgui_test.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import dearpygui.dearpygui as dpg
-import cv2 as cv
-import numpy as np
-
-dpg.create_context()
-dpg.create_viewport(title='Custom Title', width=600, height=800)
-dpg.setup_dearpygui()
-
-vid = cv.VideoCapture(0)
-ret, frame = vid.read()
-
-# image size or you can get this from image shape
-frame_width = vid.get(cv.CAP_PROP_FRAME_WIDTH)
-frame_height = vid.get(cv.CAP_PROP_FRAME_HEIGHT)
-video_fps = vid.get(cv.CAP_PROP_FPS)
-print(frame_width)
-print(frame_height)
-print(video_fps)
-
-print("Frame Array:")
-print("Array is of type: ", type(frame))
-print("No. of dimensions: ", frame.ndim)
-print("Shape of array: ", frame.shape)
-print("Size of array: ", frame.size)
-print("Array stores elements of type: ", frame.dtype)
-data = np.flip(frame, 2) # because the camera data comes in as BGR and we need RGB
-data = data.ravel() # flatten camera data to a 1 d stricture
-data = np.asarray(data, dtype=np.float32) # change data type to 32bit floats
-texture_data = np.true_divide(data, 255.0) # normalize image data to prepare for GPU
-
-print("texture_data Array:")
-print("Array is of type: ", type(texture_data))
-print("No. of dimensions: ", texture_data.ndim)
-print("Shape of array: ", texture_data.shape)
-print("Size of array: ", texture_data.size)
-print("Array stores elements of type: ", texture_data.dtype)
-
-with dpg.texture_registry(show=True):
- dpg.add_raw_texture(frame.shape[1], frame.shape[0], texture_data, tag="texture_tag", format=dpg.mvFormat_Float_rgb)
-
-with dpg.window(label="Example Window"):
- dpg.add_text("Hello, world")
- dpg.add_image("texture_tag")
-
-dpg.show_metrics()
-dpg.show_viewport()
-while dpg.is_dearpygui_running():
-
- # updating the texture in a while loop the frame rate will be limited to the camera frame rate.
- # commenting out the "ret, frame = vid.read()" line will show the full speed that operations and updating a texture can run at
-
- ret, frame = vid.read()
- data = np.flip(frame, 2)
- data = data.ravel()
- data = np.asarray(data, dtype=np.float32)
- texture_data = np.true_divide(data, 255.0)
- dpg.set_value("texture_tag", texture_data)
-
- # to compare to the base example in the open cv tutorials uncomment below
- #cv.imshow('frame', frame)
- dpg.render_dearpygui_frame()
-
-vid.release()
-#cv.destroyAllWindows() # when using upen cv window "imshow" call this also
-dpg.destroy_context()
\ No newline at end of file
diff --git a/kotonebot/__init__.py b/kotonebot/__init__.py
index b6d7acd..7946e21 100644
--- a/kotonebot/__init__.py
+++ b/kotonebot/__init__.py
@@ -1,15 +1,4 @@
-from kotonebot.client.protocol import DeviceABC
+from .client.protocol import DeviceABC
from .backend.context import ContextOcr, ContextImage, ContextDebug, device, ocr, image, debug
-from .backend.util import Rect, fuzz, regex, contains, grayscale
-
-# device: DeviceProtocol
-# ocr: ContextOcr
-# image: ContextImage
-# debug: ContextDebug
-
-# def __getattr__(name: str):
-# try:
-# return getattr(_c, name)
-# except AttributeError:
-# return globals()[name]
+from .backend.util import Rect, fuzz, regex, contains, grayscaled, grayscale_cached
diff --git a/kotonebot/backend/context.py b/kotonebot/backend/context.py
index 16bf83e..370fce4 100644
--- a/kotonebot/backend/context.py
+++ b/kotonebot/backend/context.py
@@ -28,7 +28,7 @@ class ContextOcr:
def raw(self, lang: OcrLanguage = 'jp') -> Ocr:
"""
返回 `kotonebot.backend.ocr` 中的 Ocr 对象。\n
- Ocr 对象与此对象的区别是,此对象会自动截图,而 Ocr 对象需要手动传入图像参数。
+ Ocr 对象与此对象(ContextOcr)的区别是,此对象会自动截图,而 Ocr 对象需要手动传入图像参数。
"""
match lang:
case 'jp':
@@ -201,7 +201,7 @@ class ContextImage:
time.sleep(0.1)
- def expect(self, template: str, mask: str | None = None, threshold: float = 0.9) -> TemplateMatchResult:
+ def expect(self, template: str | MatLike, mask: str | None = None, threshold: float = 0.9) -> TemplateMatchResult:
"""
寻找指定图像。
diff --git a/kotonebot/backend/util.py b/kotonebot/backend/util.py
index 09085c0..f74f836 100644
--- a/kotonebot/backend/util.py
+++ b/kotonebot/backend/util.py
@@ -76,12 +76,12 @@ def cropper_x(x1: float, x2: float) -> Callable[[MatLike], MatLike]:
return lambda img: crop_x(img, x1, x2)
-def grayscale(img: MatLike | str) -> MatLike:
+def grayscaled(img: MatLike | str) -> MatLike:
if isinstance(img, str):
img = cv2.imread(img)
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
@lru_cache
def grayscale_cached(img: MatLike | str) -> MatLike:
- return grayscale(img)
+ return grayscaled(img)
diff --git a/kotonebot/client/device/adb.py b/kotonebot/client/device/adb.py
index d2de32c..c297c6c 100644
--- a/kotonebot/client/device/adb.py
+++ b/kotonebot/client/device/adb.py
@@ -35,7 +35,7 @@ class AdbDevice(DeviceABC):
def __click_last(self) -> None:
if self.last_find is None:
- raise ValueError("No last find result")
+ raise ValueError("No last find result. Make sure you are not calling the 'raw' functions.")
self.click(self.last_find)
def __click_rect(self, rect: Rect) -> None:
diff --git a/kotonebot/tasks/actions/common.py b/kotonebot/tasks/actions/common.py
new file mode 100644
index 0000000..836505c
--- /dev/null
+++ b/kotonebot/tasks/actions/common.py
@@ -0,0 +1,132 @@
+from typing import Literal
+from logging import getLogger
+from time import sleep
+
+from kotonebot import (
+ ocr,
+ device,
+ contains,
+ image,
+ debug,
+ regex,
+ grayscaled,
+ grayscale_cached
+)
+from .. import R
+from .pdorinku import acquire_pdorinku
+
+logger = getLogger(__name__)
+
+def acquire_skill_card():
+ """获取技能卡(スキルカード)"""
+ # TODO: 识别卡片内容,而不是固定选卡
+ # TODO: 不硬编码坐标
+ CARD_POSITIONS = [
+ (157, 820, 128, 128),
+ (296, 820, 128, 128),
+ (435, 820, 128, 128),
+ ]
+ logger.info("Click first skill card")
+ device.click(CARD_POSITIONS[0])
+ sleep(0.5)
+ # 确定
+ logger.info("Click 受け取る")
+ device.click(ocr.expect(contains("受け取る")).rect)
+ # 跳过动画
+ device.click(image.expect_wait_any([
+ R.InPurodyuusu.PSkillCardIconBlue,
+ R.InPurodyuusu.PSkillCardIconColorful
+ ]))
+
+AcquisitionType = Literal[
+ "PDrinkAcquire", # P饮料被动领取
+ "PDrinkSelect", # P饮料主动领取
+ "PDrinkMax", # P饮料到达上限
+ "PSkillCardAcquire", # 技能卡领取
+ "PSkillCardSelect", # 技能卡选择
+ "PItem", # P物品
+ "Clear", # 目标达成
+]
+def acquisitions() -> AcquisitionType | None:
+ """处理行动开始前和结束后可能需要处理的事件,直到到行动页面为止"""
+ img = device.screenshot_raw()
+ gray_img = grayscaled(img)
+ logger.info("Acquisition stuffs...")
+
+ # P饮料被动领取
+ logger.info("Check PDrink acquisition...")
+ if image.raw().find(img, R.InPurodyuusu.PDrinkIcon):
+ logger.info("Click to finish animation")
+ device.click_center()
+ sleep(1)
+ return "PDrinkAcquire"
+ # P饮料主动领取
+ # if ocr.raw().find(img, contains("受け取るPドリンクを選れでください")):
+ if image.raw().find(img, R.InPurodyuusu.TextPleaseSelectPDrink):
+ logger.info("PDrink acquisition")
+ # 不领取
+ # device.click(ocr.expect(contains("受け取らない")))
+ # sleep(0.5)
+ # device.click(image.expect(R.InPurodyuusu.ButtonNotAcquire))
+ # sleep(0.5)
+ # device.click(image.expect(R.InPurodyuusu.ButtonConfirm))
+ acquire_pdorinku(index=0)
+ return "PDrinkSelect"
+ # P饮料到达上限
+ if image.raw().find(img, R.InPurodyuusu.TextPDrinkMax):
+ device.click(image.expect(R.InPurodyuusu.ButtonLeave))
+ sleep(0.7)
+ # 可能需要点击确认
+ device.click(image.expect(R.InPurodyuusu.ButtonConfirm, threshold=0.8))
+ return "PDrinkMax"
+ # 技能卡被动领取(支援卡效果)
+ logger.info("Check skill card acquisition...")
+ if image.raw().find_any(img, [
+ R.InPurodyuusu.PSkillCardIconBlue,
+ R.InPurodyuusu.PSkillCardIconColorful
+ ]):
+ logger.info("Acquire skill card")
+ device.click_center()
+ return "PSkillCardAcquire"
+ # 技能卡选择
+ if ocr.raw().find(img, contains("受け取るスキルカードを選んでください")):
+ logger.info("Acquire skill card")
+ acquire_skill_card()
+ sleep(5)
+ return "PSkillCardSelect"
+ # 奖励箱技能卡
+ if res := image.raw().find(gray_img, grayscaled(R.InPurodyuusu.LootBoxSkillCard)):
+ logger.info("Acquire skill card from loot box")
+ device.click(res.rect)
+ # 下面就是普通的技能卡选择
+ return acquisitions()
+ # 目标达成
+ if image.raw().find(gray_img, grayscale_cached(R.InPurodyuusu.IconClearBlue)):
+ logger.debug("達成: clicked")
+ device.click_center()
+ sleep(5)
+ # TODO: 可能不存在 達成 NEXT
+ logger.debug("達成 NEXT: clicked")
+ device.click_center()
+ return "Clear"
+ # P物品
+ if image.raw().find(img, R.InPurodyuusu.PItemIconColorful):
+ logger.info("Click to finish PItem acquisition")
+ device.click_center()
+ sleep(1)
+ return "PItem"
+ # 支援卡
+ # logger.info("Check support card acquisition...")
+ # 记忆
+ # 未跳过剧情
+ return None
+
+if __name__ == '__main__':
+ from logging import getLogger
+ import logging
+ from kotonebot.backend.context import init_context
+ logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s')
+ getLogger('kotonebot').setLevel(logging.DEBUG)
+ getLogger(__name__).setLevel(logging.DEBUG)
+ init_context()
+ acquisitions()
diff --git a/kotonebot/tasks/actions/in_purodyuusu.py b/kotonebot/tasks/actions/in_purodyuusu.py
index ac4247d..6146822 100644
--- a/kotonebot/tasks/actions/in_purodyuusu.py
+++ b/kotonebot/tasks/actions/in_purodyuusu.py
@@ -1,20 +1,19 @@
import random
-import re
import time
from typing import Literal
from typing_extensions import deprecated
-import numpy as np
import cv2
import unicodedata
import logging
from time import sleep
-from kotonebot import ocr, device, fuzz, contains, image, debug, regex
+from kotonebot import ocr, device, contains, image, debug, regex
from kotonebot.backend.context import init_context
-from kotonebot.backend.util import crop_y, cropper_y, grayscale, grayscale_cached
+from kotonebot.backend.util import crop_y, cropper_y
from kotonebot.tasks import R
from kotonebot.tasks.actions import loading
-from kotonebot.tasks.actions.pdorinku import acquire_pdorinku
+from .non_lesson_actions import enter_allowance, study_available, enter_study, allowance_available
+from .common import acquisitions, AcquisitionType, acquire_skill_card
logger = logging.getLogger(__name__)
@@ -299,26 +298,7 @@ def remaing_turns_and_points():
turns_ocr = ocr.ocr(turns_img)
logger.debug("turns_ocr: %s", turns_ocr)
-def acquire_skill_card():
- """获取技能卡(スキルカード)"""
- # TODO: 识别卡片内容,而不是固定选卡
- # TODO: 不硬编码坐标
- CARD_POSITIONS = [
- (157, 820, 128, 128),
- (296, 820, 128, 128),
- (435, 820, 128, 128),
- ]
- logger.info("Click first skill card")
- device.click(CARD_POSITIONS[0])
- sleep(0.5)
- # 确定
- logger.info("Click 受け取る")
- device.click(ocr.expect(contains("受け取る")).rect)
- # 跳过动画
- device.click(image.expect_wait_any([
- R.InPurodyuusu.PSkillCardIconBlue,
- R.InPurodyuusu.PSkillCardIconColorful
- ]))
+
def rest():
"""执行休息"""
@@ -328,83 +308,7 @@ def rest():
# 确定
device.click(image.expect_wait(R.InPurodyuusu.RestConfirmBtn))
-AcquisitionType = Literal[
- "PDrinkAcquire", # P饮料被动领取
- "PDrinkSelect", # P饮料主动领取
- "PDrinkMax", # P饮料到达上限
- "PSkillCardAcquire", # 技能卡被动领取
- "PSkillCardSelect", # 技能卡主动领取
- "PItem", # P物品
- "Clear", # 目标达成
-]
-def acquisitions() -> AcquisitionType | None:
- """处理行动开始前和结束后可能需要处理的事件,直到到行动页面为止"""
- img = device.screenshot_raw()
- gray_img = grayscale(img)
- logger.info("Acquisition stuffs...")
- # P饮料被动领取
- logger.info("Check PDrink acquisition...")
- if image.raw().find(img, R.InPurodyuusu.PDrinkIcon):
- logger.info("Click to finish animation")
- device.click_center()
- sleep(1)
- return "PDrinkAcquire"
- # P饮料主动领取
- # if ocr.raw().find(img, contains("受け取るPドリンクを選れでください")):
- if image.raw().find(img, R.InPurodyuusu.TextPleaseSelectPDrink):
- logger.info("PDrink acquisition")
- # 不领取
- # device.click(ocr.expect(contains("受け取らない")))
- # sleep(0.5)
- # device.click(image.expect(R.InPurodyuusu.ButtonNotAcquire))
- # sleep(0.5)
- # device.click(image.expect(R.InPurodyuusu.ButtonConfirm))
- acquire_pdorinku(index=0)
- return "PDrinkSelect"
- # P饮料到达上限
- if image.raw().find(img, R.InPurodyuusu.TextPDrinkMax):
- device.click(image.expect(R.InPurodyuusu.ButtonLeave))
- sleep(0.7)
- # 可能需要点击确认
- device.click(image.expect(R.InPurodyuusu.ButtonConfirm, threshold=0.8))
- return "PDrinkMax"
- # 技能卡被动领取
- logger.info("Check skill card acquisition...")
- if image.raw().find_any(img, [
- R.InPurodyuusu.PSkillCardIconBlue,
- R.InPurodyuusu.PSkillCardIconColorful
- ]):
- logger.info("Acquire skill card")
- device.click_center()
- return "PSkillCardAcquire"
- # 技能卡主动领取
- if ocr.raw().find(img, contains("受け取るスキルカードを選んでください")):
- logger.info("Acquire skill card")
- acquire_skill_card()
- sleep(5)
- return "PSkillCardSelect"
-
- # 目标达成
- if image.raw().find(gray_img, grayscale_cached(R.InPurodyuusu.IconClearBlue)):
- logger.debug("達成: clicked")
- device.click_center()
- sleep(5)
- # TODO: 可能不存在 達成 NEXT
- logger.debug("達成 NEXT: clicked")
- device.click_center()
- return "Clear"
- # P物品
- if image.raw().find(img, R.InPurodyuusu.PItemIconColorful):
- logger.info("Click to finish PItem acquisition")
- device.click_center()
- sleep(1)
- return "PItem"
- # 支援卡
- # logger.info("Check support card acquisition...")
- # 记忆
- # 未跳过剧情
- return None
def until_action_scene():
"""等待进入行动场景"""
@@ -663,7 +567,7 @@ def hajime_regular(week: int = -1, start_from: int = 1):
if not enter_recommended_action():
rest()
- def week_common():
+ def week_lesson():
until_action_scene()
executed_action = enter_recommended_action()
logger.info("Executed recommended action: %s", executed_action)
@@ -677,8 +581,16 @@ def hajime_regular(week: int = -1, start_from: int = 1):
rest()
until_action_scene()
+ def week_non_lesson():
+ """非练习周。可能可用行动包括:おでかけ、相談、活動支給、授業"""
+ until_action_scene()
+ if allowance_available():
+ enter_allowance()
+ # elif study_available():
+ # enter_study()
+ until_action_scene()
- def week_final():
+ def week_final_lesson():
if enter_recommended_action(final_week=True) != 'lesson':
raise ValueError("Failed to enter recommended action on final week.")
sleep(5)
@@ -709,19 +621,19 @@ def hajime_regular(week: int = -1, start_from: int = 1):
produce_end()
weeks = [
- week_common, # 1
- week_common, # 2
- week_common, # 3
- week_common, # 4
- week_final, # 5
- week_mid_exam, # 6
- week_common, # 7
- week_common, # 8
- week_common, # 9
- week_common, # 10
- week_common, # 11
- week_final, # 12
- week_final_exam, # 13
+ week_lesson, # 1: Vo.レッスン、Da.レッスン、Vi.レッスン
+ week_lesson, # 2: 授業
+ week_lesson, # 3: Vo.レッスン、Da.レッスン、Vi.レッスン、授業
+ week_non_lesson, # 4: おでかけ、相談、活動支給
+ week_final_lesson, # 5: 追い込みレッスン
+ week_mid_exam, # 6: 中間試験
+ week_non_lesson, # 7: おでかけ、活動支給
+ week_non_lesson, # 8: 授業、活動支給
+ week_lesson, # 9: Vo.レッスン、Da.レッスン、Vi.レッスン
+ week_lesson, # 10: Vo.レッスン、Da.レッスン、Vi.レッスン、授業
+ week_non_lesson, # 11: おでかけ、相談、活動支給
+ week_final_lesson, # 12: 追い込みレッスン
+ week_final_exam, # 13: 最終試験
]
if week != -1:
logger.info("Week %d started.", week)
@@ -751,13 +663,13 @@ if __name__ == '__main__':
getLogger(__name__).setLevel(logging.DEBUG)
init_context()
- while not image.wait_for_any([
- R.InPurodyuusu.TextPDiary, # 普通周
- R.InPurodyuusu.ButtonFinalPracticeDance # 离考试剩余一周
- ], timeout=2):
- logger.info("Action scene not detected. Retry...")
- acquisitions()
- sleep(3)
+ # while not image.wait_for_any([
+ # R.InPurodyuusu.TextPDiary, # 普通周
+ # R.InPurodyuusu.ButtonFinalPracticeDance # 离考试剩余一周
+ # ], timeout=2):
+ # logger.info("Action scene not detected. Retry...")
+ # acquisitions()
+ # sleep(3)
# image.wait_for_any([
# R.InPurodyuusu.TextPDiary, # 普通周
@@ -775,7 +687,7 @@ if __name__ == '__main__':
# acquisitions()
# acquire_pdorinku(0)
# image.wait_for(R.InPurodyuusu.InPractice.PDorinkuIcon)
- # hajime_regular(start_from=1)
+ hajime_regular(start_from=9)
# until_practice_scene()
# device.click(image.expect_wait_any([
# R.InPurodyuusu.PSkillCardIconBlue,
diff --git a/kotonebot/tasks/actions/non_lesson_actions.py b/kotonebot/tasks/actions/non_lesson_actions.py
new file mode 100644
index 0000000..337eb8c
--- /dev/null
+++ b/kotonebot/tasks/actions/non_lesson_actions.py
@@ -0,0 +1,69 @@
+"""
+此文件包含非练习周的行动。
+
+具体包括:おでかけ、相談、活動支給、授業
+"""
+from time import sleep
+from logging import getLogger
+
+from kotonebot import device, image, ocr, debug
+from kotonebot.tasks import R
+from .common import acquisitions, AcquisitionType
+
+logger = getLogger(__name__)
+
+def allowance_available():
+ """
+ 判断是否可以执行活動支給。
+ """
+ return image.expect(R.InPurodyuusu.ButtonTextAllowance) is not None
+
+def study_available():
+ """
+ 判断是否可以执行授業。
+ """
+ return image.expect(R.InPurodyuusu.ButtonTextStudy) is not None
+
+def enter_study():
+ """
+ 执行授業。
+ """
+ raise NotImplementedError("授業功能未实现")
+
+def enter_allowance():
+ """
+ 执行活動支給。
+
+ 前置条件:位于行动页面,且所有行动按钮清晰可见 \n
+ 结束状态:无
+ """
+ logger.info("Executing 活動支給.")
+ # 点击活動支給 [screenshots\allowance\step_1.png]
+ logger.info("Double clicking on 活動支給.")
+ device.double_click(image.expect(R.InPurodyuusu.ButtonTextAllowance), interval=1)
+ # 等待进入页面
+ sleep(3)
+ # 第一个箱子 [screenshots\allowance\step_2.png]
+ logger.info("Clicking on the first lootbox.")
+ device.click(image.expect_wait_any([
+ R.InPurodyuusu.LootboxSliverLock
+ ]))
+ while acquisitions() is None:
+ logger.info("Waiting for acquisitions finished.")
+ sleep(2)
+ # 第二个箱子
+ logger.info("Clicking on the second lootbox.")
+ device.click(image.expect_wait_any([
+ R.InPurodyuusu.LootboxSliverLock
+ ]))
+ while acquisitions() is None:
+ logger.info("Waiting for acquisitions finished.")
+ sleep(2)
+ logger.info("活動支給 completed.")
+ # 可能会出现的新动画
+ # 技能卡:[screenshots\allowance\step_4.png]
+
+
+def study():
+ """授業"""
+ pass
\ No newline at end of file
diff --git a/res/sprites/jp/in_purodyuusu/button_text_allowance.png b/res/sprites/jp/in_purodyuusu/button_text_allowance.png
new file mode 100644
index 0000000..74d3b90
Binary files /dev/null and b/res/sprites/jp/in_purodyuusu/button_text_allowance.png differ
diff --git a/res/sprites/jp/in_purodyuusu/loot_box_skill_card.png b/res/sprites/jp/in_purodyuusu/loot_box_skill_card.png
new file mode 100644
index 0000000..dc61591
Binary files /dev/null and b/res/sprites/jp/in_purodyuusu/loot_box_skill_card.png differ
diff --git a/res/sprites/jp/in_purodyuusu/lootbox_sliver_lock.png b/res/sprites/jp/in_purodyuusu/lootbox_sliver_lock.png
new file mode 100644
index 0000000..9ac59d3
Binary files /dev/null and b/res/sprites/jp/in_purodyuusu/lootbox_sliver_lock.png differ
diff --git a/screenshots/allowance/step_1.png b/screenshots/allowance/step_1.png
new file mode 100644
index 0000000..d646bbb
Binary files /dev/null and b/screenshots/allowance/step_1.png differ
diff --git a/screenshots/allowance/step_2.png b/screenshots/allowance/step_2.png
new file mode 100644
index 0000000..26860fd
Binary files /dev/null and b/screenshots/allowance/step_2.png differ
diff --git a/screenshots/allowance/step_4.png b/screenshots/allowance/step_4.png
new file mode 100644
index 0000000..9805f02
Binary files /dev/null and b/screenshots/allowance/step_4.png differ
diff --git a/tools/R.jinja2 b/tools/R.jinja2
index 57ee559..9c91aa9 100644
--- a/tools/R.jinja2
+++ b/tools/R.jinja2
@@ -14,7 +14,7 @@ class {{ class_name }}:
"""
路径:{{ attr.rel_path }}
模块:`{{ attr.class_path|join('.') }}.{{ attr.name }}`
-
+
"""
{%- elif attr.type == 'next_class' -%}
{{ attr.name }} = {{ attr.value }}