feat(task): 重构并优化培育偶像选择

原来采用逐个选择 + OCR 方法,现在采用裁剪出画面中所有卡片图像 + 直方图对比查找。新方法比原有方法快切准确。
This commit is contained in:
XcantloadX 2025-04-02 22:46:16 +08:00
parent 97688c62c8
commit b00db58f9a
15 changed files with 443 additions and 339 deletions

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ logs/
traces/
version
kotonebot/tasks/resources
cache/
##########################
# Byte-compiled / optimized / DLL files

View File

@ -1,4 +1,5 @@
graft kotonebot/tasks/sprites
graft kotonebot/tasks/resources
prune tests
prune tools
prune experiments

View File

@ -1 +1 @@
{"definitions":{"30a6f399-6999-4f04-bb77-651e0214112f":{"name":"Produce.IconPIdolLevel","displayName":"P偶像卡上的等级图标","type":"template","annotationId":"30a6f399-6999-4f04-bb77-651e0214112f","useHintRect":false},"b71165dd-7285-45a7-bb8c-d4fccee2b0ba":{"name":"Produce.KbIdolOverviewName","displayName":"P偶像总览界面偶像名称","type":"hint-box","annotationId":"b71165dd-7285-45a7-bb8c-d4fccee2b0ba","useHintRect":false}},"annotations":[{"id":"30a6f399-6999-4f04-bb77-651e0214112f","type":"rect","data":{"x1":238,"y1":742,"x2":248,"y2":753}},{"id":"b71165dd-7285-45a7-bb8c-d4fccee2b0ba","type":"rect","data":{"x1":140,"y1":16,"x2":615,"y2":97}}]}
{"definitions":{"30a6f399-6999-4f04-bb77-651e0214112f":{"name":"Produce.IconPIdolLevel","displayName":"P偶像卡上的等级图标","type":"template","annotationId":"30a6f399-6999-4f04-bb77-651e0214112f","useHintRect":false},"b71165dd-7285-45a7-bb8c-d4fccee2b0ba":{"name":"Produce.KbIdolOverviewName","displayName":"P偶像总览界面偶像名称","type":"hint-box","annotationId":"b71165dd-7285-45a7-bb8c-d4fccee2b0ba","useHintRect":false},"46607060-cefc-43a0-abaf-9d4887d46cb8":{"name":"Produce.BoxIdolOverviewIdols","displayName":"偶像总览 偶像列表区域","type":"hint-box","annotationId":"46607060-cefc-43a0-abaf-9d4887d46cb8","useHintRect":false}},"annotations":[{"id":"30a6f399-6999-4f04-bb77-651e0214112f","type":"rect","data":{"x1":238,"y1":742,"x2":248,"y2":753}},{"id":"b71165dd-7285-45a7-bb8c-d4fccee2b0ba","type":"rect","data":{"x1":140,"y1":16,"x2":615,"y2":97}},{"id":"46607060-cefc-43a0-abaf-9d4887d46cb8","type":"rect","data":{"x1":26,"y1":568,"x2":696,"y2":992}}]}

View File

@ -35,271 +35,6 @@ class APShopItems(IntEnum):
REGENERATE_MEMORY = 3
"""回忆再生成券"""
倉本千奈_BASE = 0
十王星南_BASE = 100
姫崎莉波_BASE = 200
月村手毬_BASE = 300
有村麻央_BASE = 400
篠泽广_BASE = 500
紫云清夏_BASE = 600
花海佑芽_BASE = 700
花海咲季_BASE = 800
葛城リーリヤ_BASE = 900
藤田ことね_BASE = 1000
class PIdol(IntEnum):
"""P偶像"""
倉本千奈_Campusmode = 倉本千奈_BASE + 0
倉本千奈_WonderScale = 倉本千奈_BASE + 1
倉本千奈_ようこそ初星温泉 = 倉本千奈_BASE + 2
倉本千奈_仮装狂騒曲 = 倉本千奈_BASE + 3
倉本千奈_初心 = 倉本千奈_BASE + 4
倉本千奈_学園生活 = 倉本千奈_BASE + 5
倉本千奈_日々_発見的ステップ = 倉本千奈_BASE + 6
倉本千奈_胸を張って一歩ずつ = 倉本千奈_BASE + 7
十王星南_Campusmode = 十王星南_BASE + 0
十王星南_一番星 = 十王星南_BASE + 1
十王星南_学園生活 = 十王星南_BASE + 2
十王星南_小さな野望 = 十王星南_BASE + 3
姫崎莉波_clumsytrick = 姫崎莉波_BASE + 0
姫崎莉波_私らしさのはじまり = 姫崎莉波_BASE + 1
姫崎莉波_キミとセミブルー = 姫崎莉波_BASE + 2
姫崎莉波_Campusmode = 姫崎莉波_BASE + 3
姫崎莉波_LUV = 姫崎莉波_BASE + 4
姫崎莉波_ようこそ初星温泉 = 姫崎莉波_BASE + 5
姫崎莉波_ハッピーミルフィーユ = 姫崎莉波_BASE + 6
姫崎莉波_初心 = 姫崎莉波_BASE + 7
姫崎莉波_学園生活 = 姫崎莉波_BASE + 8
月村手毬_Lunasaymaybe = 月村手毬_BASE + 0
月村手毬_一匹狼 = 月村手毬_BASE + 1
月村手毬_Campusmode = 月村手毬_BASE + 2
月村手毬_アイヴイ = 月村手毬_BASE + 3
月村手毬_初声 = 月村手毬_BASE + 4
月村手毬_学園生活 = 月村手毬_BASE + 5
月村手毬_仮装狂騒曲 = 月村手毬_BASE + 6
有村麻央_Fluorite = 有村麻央_BASE + 0
有村麻央_はじまりはカッコよく = 有村麻央_BASE + 1
有村麻央_Campusmode = 有村麻央_BASE + 2
有村麻央_FeelJewelDream = 有村麻央_BASE + 3
有村麻央_キミとセミブルー = 有村麻央_BASE + 4
有村麻央_初恋 = 有村麻央_BASE + 5
有村麻央_学園生活 = 有村麻央_BASE + 6
篠泽广_コントラスト = 篠泽广_BASE + 0
篠泽广_一番向いていないこと = 篠泽广_BASE + 1
篠泽广_光景 = 篠泽广_BASE + 2
篠泽广_Campusmode = 篠泽广_BASE + 3
篠泽广_仮装狂騒曲 = 篠泽广_BASE + 4
篠泽广_ハッピーミルフィーユ = 篠泽广_BASE + 5
篠泽广_初恋 = 篠泽广_BASE + 6
篠泽广_学園生活 = 篠泽广_BASE + 7
紫云清夏_TameLieOneStep = 紫云清夏_BASE + 0
紫云清夏_カクシタワタシ = 紫云清夏_BASE + 1
紫云清夏_夢へのリスタート = 紫云清夏_BASE + 2
紫云清夏_Campusmode = 紫云清夏_BASE + 3
紫云清夏_キミとセミブルー = 紫云清夏_BASE + 4
紫云清夏_初恋 = 紫云清夏_BASE + 5
紫云清夏_学園生活 = 紫云清夏_BASE + 6
花海佑芽_WhiteNightWhiteWish = 花海佑芽_BASE + 0
花海佑芽_学園生活 = 花海佑芽_BASE + 1
花海佑芽_Campusmode = 花海佑芽_BASE + 2
花海佑芽_TheRollingRiceball = 花海佑芽_BASE + 3
花海佑芽_アイドル_はじめっ = 花海佑芽_BASE + 4
花海咲季_BoomBoomPow = 花海咲季_BASE + 0
花海咲季_Campusmode = 花海咲季_BASE + 1
花海咲季_FightingMyWay = 花海咲季_BASE + 2
花海咲季_わたしが一番 = 花海咲季_BASE + 3
花海咲季_冠菊 = 花海咲季_BASE + 4
花海咲季_初声 = 花海咲季_BASE + 5
花海咲季_古今東西ちょちょいのちょい = 花海咲季_BASE + 6
花海咲季_学園生活 = 花海咲季_BASE + 7
葛城リーリヤ_一つ踏み出した先に = 葛城リーリヤ_BASE + 0
葛城リーリヤ_白線 = 葛城リーリヤ_BASE + 1
葛城リーリヤ_Campusmode = 葛城リーリヤ_BASE + 2
葛城リーリヤ_WhiteNightWhiteWish = 葛城リーリヤ_BASE + 3
葛城リーリヤ_冠菊 = 葛城リーリヤ_BASE + 4
葛城リーリヤ_初心 = 葛城リーリヤ_BASE + 5
葛城リーリヤ_学園生活 = 葛城リーリヤ_BASE + 6
藤田ことね_カワイイ_はじめました = 藤田ことね_BASE + 0
藤田ことね_世界一可愛い私 = 藤田ことね_BASE + 1
藤田ことね_Campusmode = 藤田ことね_BASE + 2
藤田ことね_YellowBigBang = 藤田ことね_BASE + 3
藤田ことね_WhiteNightWhiteWish = 藤田ことね_BASE + 4
藤田ことね_冠菊 = 藤田ことね_BASE + 5
藤田ことね_初声 = 藤田ことね_BASE + 6
藤田ことね_学園生活 = 藤田ことね_BASE + 7
def to_title(self) -> list[str]:
match self:
case PIdol.倉本千奈_Campusmode:
return ["倉本", "千奈", "Campus", "mode"]
case PIdol.倉本千奈_WonderScale:
return ["倉本", "千奈", "Wonder", "Scale"]
case PIdol.倉本千奈_ようこそ初星温泉:
return ["倉本", "千奈", "ようこそ初星温泉"]
case PIdol.倉本千奈_仮装狂騒曲:
return ["倉本", "千奈", "仮装狂騒曲"]
case PIdol.倉本千奈_初心:
return ["倉本", "千奈", "初心"]
case PIdol.倉本千奈_学園生活:
return ["倉本", "千奈", "学園生活"]
case PIdol.倉本千奈_日々_発見的ステップ:
return ["倉本", "千奈", "日々、発見的ステップ"]
case PIdol.倉本千奈_胸を張って一歩ずつ:
return ["倉本", "千奈", "胸を張って一歩ずつ"]
case PIdol.十王星南_Campusmode:
return ["十王", "星南", "Campus", "mode"]
case PIdol.十王星南_一番星:
return ["十王", "星南", "一番星"]
case PIdol.十王星南_学園生活:
return ["十王", "星南", "学園生活"]
case PIdol.十王星南_小さな野望:
return ["十王", "星南", "小さな野望"]
case PIdol.姫崎莉波_clumsytrick:
return ["姫崎", "莉波", "clumsy", "trick"]
case PIdol.姫崎莉波_私らしさのはじまり:
return ["姫崎", "莉波", "『私らしさ』のはじまり"]
case PIdol.姫崎莉波_キミとセミブルー:
return ["姫崎", "莉波", "キミとセミブルー"]
case PIdol.姫崎莉波_Campusmode:
return ["姫崎", "莉波", "Campus", "mode"]
case PIdol.姫崎莉波_LUV:
return ["姫崎", "莉波", "L", "U", "V"]
case PIdol.姫崎莉波_ようこそ初星温泉:
return ["姫崎", "莉波", "ようこそ初星温泉"]
case PIdol.姫崎莉波_ハッピーミルフィーユ:
return ["姫崎", "莉波", "ハッピーミルフィーユ"]
case PIdol.姫崎莉波_初心:
return ["姫崎", "莉波", "初心"]
case PIdol.姫崎莉波_学園生活:
return ["姫崎", "莉波", "学園生活"]
case PIdol.月村手毬_Lunasaymaybe:
return ["月村", "手毬", "Luna", "say", "maybe"]
case PIdol.月村手毬_一匹狼:
return ["月村", "手毬", "一匹狼"]
case PIdol.月村手毬_Campusmode:
return ["月村", "手毬", "Campus", "mode"]
case PIdol.月村手毬_アイヴイ:
return ["月村", "手毬", "アイヴイ"]
case PIdol.月村手毬_初声:
return ["月村", "手毬", "初声"]
case PIdol.月村手毬_学園生活:
return ["月村", "手毬", "学園生活"]
case PIdol.月村手毬_仮装狂騒曲:
return ["月村", "手毬", "仮装狂騒曲"]
case PIdol.有村麻央_Fluorite:
return ["有村", "麻央", "Fluorite"]
case PIdol.有村麻央_はじまりはカッコよく:
return ["有村", "麻央", "はじまりはカッコよく"]
case PIdol.有村麻央_Campusmode:
return ["有村", "麻央", "Campus", "mode"]
case PIdol.有村麻央_FeelJewelDream:
return ["有村", "麻央", "Feel", "Jewel", "Dream"]
case PIdol.有村麻央_キミとセミブルー:
return ["有村", "麻央", "キミとセミブルー"]
case PIdol.有村麻央_初恋:
return ["有村", "麻央", "初恋"]
case PIdol.有村麻央_学園生活:
return ["有村", "麻央", "学園生活"]
case PIdol.篠泽广_コントラスト:
return ["篠泽", "", "コントラスト"]
case PIdol.篠泽广_一番向いていないこと:
return ["篠泽", "", "一番向いていないこと"]
case PIdol.篠泽广_光景:
return ["篠泽", "", "光景"]
case PIdol.篠泽广_Campusmode:
return ["篠泽", "", "Campus", "mode"]
case PIdol.篠泽广_仮装狂騒曲:
return ["篠泽", "", "仮装狂騒曲"]
case PIdol.篠泽广_ハッピーミルフィーユ:
return ["篠泽", "", "ハッピーミルフィーユ"]
case PIdol.篠泽广_初恋:
return ["篠泽", "", "初恋"]
case PIdol.篠泽广_学園生活:
return ["篠泽", "", "学園生活"]
case PIdol.紫云清夏_TameLieOneStep:
return ["紫云", "清夏", "Tame", "Lie", "One", "Step"]
case PIdol.紫云清夏_カクシタワタシ:
return ["紫云", "清夏", "カクシタワタシ"]
case PIdol.紫云清夏_夢へのリスタート:
return ["紫云", "清夏", "夢へのリスタート"]
case PIdol.紫云清夏_Campusmode:
return ["紫云", "清夏", "Campus", "mode"]
case PIdol.紫云清夏_キミとセミブルー:
return ["紫云", "清夏", "キミとセミブルー"]
case PIdol.紫云清夏_初恋:
return ["紫云", "清夏", "初恋"]
case PIdol.紫云清夏_学園生活:
return ["紫云", "清夏", "学園生活"]
case PIdol.花海佑芽_WhiteNightWhiteWish:
return ["花海", "佑芽", "White", "Night", "Wish"]
case PIdol.花海佑芽_学園生活:
return ["花海", "佑芽", "学園生活"]
case PIdol.花海佑芽_Campusmode:
return ["花海", "佑芽", "Campus", "mode"]
case PIdol.花海佑芽_TheRollingRiceball:
return ["花海", "佑芽", "The", "Rolling", "Riceball"]
case PIdol.花海佑芽_アイドル_はじめっ:
return ["花海", "佑芽", "アイドル、はじめっ"]
case PIdol.花海咲季_BoomBoomPow:
return ["花海", "咲季", "Boom", "Boom", "Pow"]
case PIdol.花海咲季_Campusmode:
return ["花海", "咲季", "Campus", "mode"]
case PIdol.花海咲季_FightingMyWay:
return ["花海", "咲季", "Fighting", "My", "Way"]
case PIdol.花海咲季_わたしが一番:
return ["花海", "咲季", "わたしが一番"]
case PIdol.花海咲季_冠菊:
return ["花海", "咲季", "冠菊"]
case PIdol.花海咲季_初声:
return ["花海", "咲季", "初声"]
case PIdol.花海咲季_古今東西ちょちょいのちょい:
return ["花海", "咲季", "古今東西ちょちょいのちょい"]
case PIdol.花海咲季_学園生活:
return ["花海", "咲季", "学園生活"]
case PIdol.葛城リーリヤ_一つ踏み出した先に:
return ["葛城", "リーリヤ", "一つ踏み出した先に"]
case PIdol.葛城リーリヤ_白線:
return ["葛城", "リーリヤ", "白線"]
case PIdol.葛城リーリヤ_Campusmode:
return ["葛城", "リーリヤ", "Campus", "mode"]
case PIdol.葛城リーリヤ_WhiteNightWhiteWish:
return ["葛城", "リーリヤ", "White", "Night", "Wish"]
case PIdol.葛城リーリヤ_冠菊:
return ["葛城", "リーリヤ", "冠菊"]
case PIdol.葛城リーリヤ_初心:
return ["葛城", "リーリヤ", "初心"]
case PIdol.葛城リーリヤ_学園生活:
return ["葛城", "リーリヤ", "学園生活"]
case PIdol.藤田ことね_カワイイ_はじめました:
return ["藤田", "ことね", "カワイイ", "はじめました"]
case PIdol.藤田ことね_世界一可愛い私:
return ["藤田", "ことね", "世界一可愛い私"]
case PIdol.藤田ことね_Campusmode:
return ["藤田", "ことね", "Campus", "mode"]
case PIdol.藤田ことね_YellowBigBang:
return ["藤田", "ことね", "Yellow", "Big", "Bang"]
case PIdol.藤田ことね_WhiteNightWhiteWish:
return ["藤田", "ことね", "White", "Night", "Wish"]
case PIdol.藤田ことね_冠菊:
return ["藤田", "ことね", "冠菊"]
case PIdol.藤田ことね_初声:
return ["藤田", "ことね", "初声"]
case PIdol.藤田ことね_学園生活:
return ["藤田", "ことね", "学園生活"]
case _:
assert_never(self)
class DailyMoneyShopItems(IntEnum):
"""日常商店物品"""
Recommendations = -1
@ -582,9 +317,9 @@ class ProduceConfig(ConfigBaseModel):
"""
produce_count: int = 1
"""培育的次数。"""
idols: list[PIdol] = []
idols: list[str] = []
"""
要培育偶像将会按顺序循环选择培育
要培育偶像IdolCardSkin.id将会按顺序循环选择培育
若未选择任何偶像则使用游戏默认选择的偶像为上次培育偶像
"""
memory_sets: list[int] = []
@ -760,7 +495,114 @@ def upgrade_config() -> str | None:
with open('config.json', 'w', encoding='utf-8') as f:
json.dump(root, f, ensure_ascii=False, indent=4)
return '\n'.join(messages)
倉本千奈_BASE = 0
十王星南_BASE = 100
姫崎莉波_BASE = 200
月村手毬_BASE = 300
有村麻央_BASE = 400
篠泽广_BASE = 500
紫云清夏_BASE = 600
花海佑芽_BASE = 700
花海咲季_BASE = 800
葛城リーリヤ_BASE = 900
藤田ことね_BASE = 1000
class PIdol(IntEnum):
"""
P偶像已废弃仅为 upgrade_v1_to_v2() 使用而保留
"""
倉本千奈_Campusmode = 倉本千奈_BASE + 0
倉本千奈_WonderScale = 倉本千奈_BASE + 1
倉本千奈_ようこそ初星温泉 = 倉本千奈_BASE + 2
倉本千奈_仮装狂騒曲 = 倉本千奈_BASE + 3
倉本千奈_初心 = 倉本千奈_BASE + 4
倉本千奈_学園生活 = 倉本千奈_BASE + 5
倉本千奈_日々_発見的ステップ = 倉本千奈_BASE + 6
倉本千奈_胸を張って一歩ずつ = 倉本千奈_BASE + 7
十王星南_Campusmode = 十王星南_BASE + 0
十王星南_一番星 = 十王星南_BASE + 1
十王星南_学園生活 = 十王星南_BASE + 2
十王星南_小さな野望 = 十王星南_BASE + 3
姫崎莉波_clumsytrick = 姫崎莉波_BASE + 0
姫崎莉波_私らしさのはじまり = 姫崎莉波_BASE + 1
姫崎莉波_キミとセミブルー = 姫崎莉波_BASE + 2
姫崎莉波_Campusmode = 姫崎莉波_BASE + 3
姫崎莉波_LUV = 姫崎莉波_BASE + 4
姫崎莉波_ようこそ初星温泉 = 姫崎莉波_BASE + 5
姫崎莉波_ハッピーミルフィーユ = 姫崎莉波_BASE + 6
姫崎莉波_初心 = 姫崎莉波_BASE + 7
姫崎莉波_学園生活 = 姫崎莉波_BASE + 8
月村手毬_Lunasaymaybe = 月村手毬_BASE + 0
月村手毬_一匹狼 = 月村手毬_BASE + 1
月村手毬_Campusmode = 月村手毬_BASE + 2
月村手毬_アイヴイ = 月村手毬_BASE + 3
月村手毬_初声 = 月村手毬_BASE + 4
月村手毬_学園生活 = 月村手毬_BASE + 5
月村手毬_仮装狂騒曲 = 月村手毬_BASE + 6
有村麻央_Fluorite = 有村麻央_BASE + 0
有村麻央_はじまりはカッコよく = 有村麻央_BASE + 1
有村麻央_Campusmode = 有村麻央_BASE + 2
有村麻央_FeelJewelDream = 有村麻央_BASE + 3
有村麻央_キミとセミブルー = 有村麻央_BASE + 4
有村麻央_初恋 = 有村麻央_BASE + 5
有村麻央_学園生活 = 有村麻央_BASE + 6
篠泽广_コントラスト = 篠泽广_BASE + 0
篠泽广_一番向いていないこと = 篠泽广_BASE + 1
篠泽广_光景 = 篠泽广_BASE + 2
篠泽广_Campusmode = 篠泽广_BASE + 3
篠泽广_仮装狂騒曲 = 篠泽广_BASE + 4
篠泽广_ハッピーミルフィーユ = 篠泽广_BASE + 5
篠泽广_初恋 = 篠泽广_BASE + 6
篠泽广_学園生活 = 篠泽广_BASE + 7
紫云清夏_TameLieOneStep = 紫云清夏_BASE + 0
紫云清夏_カクシタワタシ = 紫云清夏_BASE + 1
紫云清夏_夢へのリスタート = 紫云清夏_BASE + 2
紫云清夏_Campusmode = 紫云清夏_BASE + 3
紫云清夏_キミとセミブルー = 紫云清夏_BASE + 4
紫云清夏_初恋 = 紫云清夏_BASE + 5
紫云清夏_学園生活 = 紫云清夏_BASE + 6
花海佑芽_WhiteNightWhiteWish = 花海佑芽_BASE + 0
花海佑芽_学園生活 = 花海佑芽_BASE + 1
花海佑芽_Campusmode = 花海佑芽_BASE + 2
花海佑芽_TheRollingRiceball = 花海佑芽_BASE + 3
花海佑芽_アイドル_はじめっ = 花海佑芽_BASE + 4
花海咲季_BoomBoomPow = 花海咲季_BASE + 0
花海咲季_Campusmode = 花海咲季_BASE + 1
花海咲季_FightingMyWay = 花海咲季_BASE + 2
花海咲季_わたしが一番 = 花海咲季_BASE + 3
花海咲季_冠菊 = 花海咲季_BASE + 4
花海咲季_初声 = 花海咲季_BASE + 5
花海咲季_古今東西ちょちょいのちょい = 花海咲季_BASE + 6
花海咲季_学園生活 = 花海咲季_BASE + 7
葛城リーリヤ_一つ踏み出した先に = 葛城リーリヤ_BASE + 0
葛城リーリヤ_白線 = 葛城リーリヤ_BASE + 1
葛城リーリヤ_Campusmode = 葛城リーリヤ_BASE + 2
葛城リーリヤ_WhiteNightWhiteWish = 葛城リーリヤ_BASE + 3
葛城リーリヤ_冠菊 = 葛城リーリヤ_BASE + 4
葛城リーリヤ_初心 = 葛城リーリヤ_BASE + 5
葛城リーリヤ_学園生活 = 葛城リーリヤ_BASE + 6
藤田ことね_カワイイ_はじめました = 藤田ことね_BASE + 0
藤田ことね_世界一可愛い私 = 藤田ことね_BASE + 1
藤田ことね_Campusmode = 藤田ことね_BASE + 2
藤田ことね_YellowBigBang = 藤田ことね_BASE + 3
藤田ことね_WhiteNightWhiteWish = 藤田ことね_BASE + 4
藤田ことね_冠菊 = 藤田ことね_BASE + 5
藤田ことね_初声 = 藤田ことね_BASE + 6
藤田ことね_学園生活 = 藤田ことね_BASE + 7
def upgrade_v1_to_v2(options: dict[str, Any]) -> tuple[dict[str, Any], str | None]:
"""
v1 -> v2 变更

View File

@ -0,0 +1,2 @@
from .idol_card import IdolCard
from .constants import CharacterId

View File

@ -0,0 +1,14 @@
from enum import Enum
class CharacterId(Enum):
hski = "hski" # Hanami Saki, 花海咲季
ttmr = "ttmr" # Tsukimura Temari, 月村手毬
fktn = "fktn" # Fujita Kotone, 藤田ことね
amao = "amao" # Arimura Mao, 有村麻央
kllj = "kllj" # Katsuragi Lilja, 葛城リーリヤ
kcna = "kcna" # Kuramoto China, 倉本千奈
ssmk = "ssmk" # Shiun Sumika, 紫云清夏
shro = "shro" # Shinosawa Hiro, 篠澤廣
hrnm = "hrnm" # Himesaki Rinami, 姫崎莉波
hume = "hume" # Hanami Ume, 花海佑芽
jsna = "jsna" # Juo Sena, 十王星南

View File

@ -0,0 +1,60 @@
from dataclasses import dataclass
from .sqlite import select, select_many
from .constants import CharacterId
@dataclass
class IdolCard:
"""偶像卡"""
id: str
skin_id: str
is_another: bool
another_name: str | None
name: str
@classmethod
def from_skin_id(cls, sid: str) -> 'IdolCard | None':
"""
根据 skin_id 查询 IdolCard
"""
row = select("""
SELECT
IC.id AS cardId,
ICS.id AS skinId,
Char.lastName || ' ' || Char.firstName || ' ' || IC.name AS name,
NOT (IC.originalIdolCardSkinId = ICS.id) AS isAnotherVer,
ICS.name AS anotherVerName
FROM IdolCard IC
JOIN Character Char ON characterId = Char.id
JOIN IdolCardSkin ICS ON IC.id = ICS.idolCardId
WHERE ICS.id = ?;
""", sid)
if row is None:
return None
card_id, skin_id, name, is_another, another_name = row
return cls(card_id, skin_id, is_another, another_name, name)
@classmethod
def all(cls) -> list['IdolCard']:
"""获取所有偶像卡"""
rows = select_many("""
SELECT
IC.id AS cardId,
ICS.id AS skinId,
Char.lastName || ' ' || Char.firstName || ' ' || IC.name AS name,
NOT (IC.originalIdolCardSkinId = ICS.id) AS isAnotherVer,
ICS.name AS anotherVerName
FROM IdolCard IC
JOIN Character Char ON characterId = Char.id
JOIN IdolCardSkin ICS ON IC.id = ICS.idolCardId;
""")
results = []
for row in rows:
card_id, skin_id, name, is_another, another_name = row
results.append(cls(card_id, skin_id, is_another, another_name, name))
return results
if __name__ == '__main__':
from pprint import pprint as print
print(IdolCard.from_skin_id('i_card-skin-fktn-3-006'))
print(IdolCard.all())

View File

@ -0,0 +1,25 @@
import os
import sqlite3
from typing import Any, cast
from kotonebot.tasks import resources as res
_db: sqlite3.Connection | None = None
_db_path = cast(str, res.__path__)[0] + '/game.db'
def select_many(query: str, *args) -> list[Any]:
global _db
if not _db:
_db = sqlite3.connect(_db_path)
c = _db.cursor()
c.execute(query, args)
return c.fetchall()
def select(query: str, *args) -> Any:
global _db
if not _db:
_db = sqlite3.connect(_db_path)
c = _db.cursor()
c.execute(query, args)
return c.fetchone()

View File

@ -1,3 +1,4 @@
from .toolbar import toolbar_home, toolbar_menu
from .commu_event_buttons import CommuEventButtonUI
from .common import WhiteFilter
from .scrollable import Scrollable, ScrollableIterator

View File

@ -0,0 +1,166 @@
import os
import logging
from typing import cast
from importlib import resources
import cv2
import numpy as np
from cv2.typing import MatLike
from kotonebot.tasks import R
from kotonebot.tasks.util import paths
from kotonebot.util import Rect, cv2_imread
from kotonebot.tasks.game_ui import Scrollable
from kotonebot.backend.debug import result, img
from kotonebot import device, color, action, sleep, contains
from kotonebot.tasks.image_db import ImageDatabase, HistDescriptor, FileDataSource
from kotonebot.backend.preprocessor import HsvColorRemover, HsvColorsRemover
logger = logging.getLogger(__name__)
_db: ImageDatabase | None = None
# OpenCV HSV 颜色范围
RED_DOT = ((157, 205, 255), (179, 255, 255)) # 红点
ORANGE_SELECT_BORDER = ((9, 50, 106), (19, 255, 255)) # 当前选中的偶像的橙色边框
WHITE_BACKGROUND = ((0, 0, 234), (179, 40, 255)) # 白色背景
def extract_idols(img: MatLike) -> list[Rect]:
"""
寻找给定图像中的所有偶像
:img: 输入图像格式为 BGR 720x1280
:return: 所有偶像的矩形区域 `(x, y, w, h)`如果未找到则返回空列表
"""
# 移除不需要的颜色
remover = HsvColorsRemover([RED_DOT, ORANGE_SELECT_BORDER, WHITE_BACKGROUND])
img = remover.process(img)
# 灰度、查找轮廓
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
contours, _ = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 筛选面积、比例约为 140x190 的轮廓
rects = []
target_ratio = 140 / 190 # 目标宽高比
target_area = 140 * 190 # 目标面积
ratio_tolerance = 0.1 # 允许的误差范围
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if h == 0:
continue
ratio = w / h
if abs(ratio - target_ratio) <= ratio_tolerance and w * h >= target_area:
rects.append((x, y, w, h))
return rects
def display_rects(img: MatLike, rects: list[Rect]) -> MatLike:
"""Draw rectangles on the image and display them."""
result = img.copy()
for rect in rects:
x, y, w, h = rect
# Draw rectangle with green color and 2px thickness
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 255, 0), 2)
# Optionally add text label
cv2.putText(result, f"{w}x{h}", (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
return result
def draw_idol_preview(img: MatLike, rects: list[Rect], db: ImageDatabase, idol_path: str) -> MatLike:
"""
在预览图上绘制所有匹配到的偶像
:param img: 原始图像
:param rects: 检测到的偶像矩形区域列表
:param db: 偶像图像数据库
:param idol_path: 偶像图像文件路径
:return: 带有匹配偶像的预览图
"""
# 创建一个与原图大小相同的白色背景图片
preview_img = np.ones_like(img) * 255
# 在预览图上绘制所有匹配到的偶像
for rect in rects:
x, y, w, h = rect
idol_img = img[y:y+h, x:x+w]
match = db.match(idol_img, 20)
if not match:
continue
file = os.path.join(idol_path, match.key)
found_img = cv2_imread(file)
# 将找到的偶像图片缩放至与检测到的矩形大小相同
resized_found_img = cv2.resize(found_img, (w, h))
# 将缩放后的图片放到预览图上对应位置
preview_img[y:y+h, x:x+w] = resized_found_img
# 在预览图上绘制矩形框
cv2.rectangle(preview_img, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 可选添加偶像ID标签
cv2.putText(preview_img, match.key.split('.')[0], (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
return preview_img
def idols_db() -> ImageDatabase:
global _db
if _db is None:
logger.info('Loading idols database...')
path = paths.resource('idol_cards')
db_path = paths.cache('idols.pkl')
_db = ImageDatabase(FileDataSource(str(path)), db_path, HistDescriptor(8), name='idols')
return _db
@action('定位偶像', screenshot_mode='manual-inherit')
def locate_idol(skin_id: str):
device.screenshot()
logger.info('Locating idol %s', skin_id)
x, y, w, h = R.Produce.BoxIdolOverviewIdols
db = idols_db()
sc = Scrollable(color_schema='light')
sc.update()
logger.debug('Idol preview pages count: %s', repr(sc.page_count))
pc = sc.page_count
assert pc is not None
# 1280x720 分辨率下,一行 4 个,一页共 12 个。
# 一次只翻 0.8 行。
for _ in sc(4 / (pc * 12) * 0.8):
img = device.screenshot()
# 只保留 BoxIdolOverviewIdols 区域
mask = np.zeros_like(img)
mask[y:y+h, x:x+w] = img[y:y+h, x:x+w]
img = mask
# 检测 & 查询
rects = extract_idols(img)
# cv2.imshow('Detected Idols', cv2.resize(display_rects(img, rects), (0, 0), fx=0.5, fy=0.5))
# cv2.imshow('Idols Preview', cv2.resize(draw_idol_preview(img, rects, db, paths.resource('idol_cards')), (0, 0), fx=0.5, fy=0.5))
# cv2.waitKey(0)
for rect in rects:
rx, ry, rw, rh = rect
idol_img = img[ry:ry+rh, rx:rx+rw]
match = db.match(idol_img, 20)
logger.debug('Result rect: %s, match: %s', repr(rect), repr(match))
# Key 格式:{skin_id}_{index}
# 同一张卡升级前后图片不一样index 分别为 0 和 1
if match and match.key.startswith(skin_id):
logger.info('Found idol %s', skin_id)
return rx, ry, rw, rh
return None
# cv2.imshow('Detected Idols', cv2.resize(display_rects(img, rects), (0, 0), fx=0.5, fy=0.5))
# # 使用新函数绘制预览图
# preview_img = draw_idol_preview(img, rects, db, path)
def test():
from kotonebot.backend.context import init_context, manual_context, device
init_context()
manual_context().begin()
locate_idol('i_card-skin-fktn-3-006')
if __name__ == '__main__':
from pprint import pprint as print
from kotonebot.util import cv2_imread
from kotonebot.backend.preprocessor import HsvColorFilter
test()

View File

@ -1,4 +1,4 @@
from .db import ImageDatabase, Db, DatabaseQueryResult
from .db import ImageDatabase, Db, DatabaseQueryResult, FileDataSource, DataSource
from .descriptors import HistDescriptor
__all__ = ['ImageDatabase', 'Db', 'DatabaseQueryResult', 'HistDescriptor']
__all__ = ['ImageDatabase', 'Db', 'DatabaseQueryResult', 'HistDescriptor', 'FileDataSource', 'DataSource']

View File

@ -52,6 +52,9 @@ class DatabaseQueryResult(NamedTuple):
feature: Any
distance: float
def __repr__(self):
return f'DatabaseQueryResult(key={self.key}, distance={self.distance})'
def chi2_distance(hist1: np.ndarray, hist2: np.ndarray, eps=1e-10):
return 0.5 * np.sum((hist1 - hist2) ** 2 / (hist1 + hist2 + eps))
@ -163,7 +166,7 @@ class ImageDatabase:
if __name__ == '__main__':
from kotonebot.tasks.db.image_db.db import Db
from kotonebot.tasks.image_db.db import Db
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
imgs_path = r'E:\GithubRepos\KotonesAutoAssistant.worktrees\dev\kotonebot\tasks\resources\idol_cards'
needle_path = r'D:\05.png'

View File

@ -11,6 +11,7 @@ from kotonebot.backend.dispatch import SimpleDispatcher
from .. import R
from ..common import conf, PIdol
from ..actions.scenes import at_home, goto_home
from ..game_ui.idols_overview import locate_idol
from ..produce.in_purodyuusu import hajime_pro, hajime_regular, resume_regular_produce
from kotonebot import device, image, ocr, task, action, sleep, contains
@ -37,69 +38,33 @@ def unify(arr: list[int]):
return result
@action('选择P偶像', screenshot_mode='manual-inherit')
def select_idol(target_titles: list[str] | PIdol):
def select_idol(skin_id: str):
"""
选择目标P偶像
前置条件培育-偶像选择页面 1.アイドル選択\n
结束状态培育-偶像选择页面 1.アイドル選択\n
:param target_titles: 目标偶像的名称关键字选择时只会选择所有关键字都出现的偶像
前置条件偶像选择页面 1.アイドル選択\n
结束状态偶像选择页面 1.アイドル選択\n
"""
# 前置条件:[res/sprites/jp/produce/produce_preparation1.png]
# 结束状态:[res/sprites/jp/produce/produce_preparation1.png]
logger.info(f"Find and select idol: {target_titles}")
logger.info("Find and select idol: %s", skin_id)
# 进入总览
device.screenshot()
device.click(image.expect(R.Produce.ButtonPIdolOverview))
it = Interval()
while not image.find(R.Common.ButtonConfirmNoIcon):
if image.find(R.Produce.ButtonPIdolOverview):
device.click()
device.screenshot()
if isinstance(target_titles, PIdol):
target_titles = target_titles.to_title()
_target_titles = [contains(t, ignore_case=True) for t in target_titles]
device.screenshot()
# 定位滑动基准
results = image.find_all(R.Produce.IconPIdolLevel)
results.sort(key=lambda r: tuple(r.position))
ys = unify([r.position[1] for r in results])
min_y = ys[0]
max_y = ys[1]
found = False
max_tries = 5
tries = 0
sc = Scrollable()
# 找到目标偶像
while not found:
# 首先检查当前选中的是不是已经是目标
if all(ocr.find_all(_target_titles, rect=R.Produce.KbIdolOverviewName)):
found = True
break
# 如果不是,就挨个选中,判断名称
for r in results:
device.click(r)
sleep(0.3)
device.screenshot(force=True)
if all(ocr.find_all(_target_titles, rect=R.Produce.KbIdolOverviewName)):
found = True
break
if not found:
tries += 1
if tries > max_tries:
break
# 翻页
# device.swipe(x1=100, x2=100, y1=max_y, y2=min_y)
sc.next(page=0.8)
sleep(0.4)
device.screenshot()
results = image.find_all(R.Produce.IconPIdolLevel)
results.sort(key=lambda r: tuple(r.position))
device.click(image.expect(R.Common.ButtonConfirmNoIcon))
return found
it.wait()
# 选择偶像
pos = locate_idol(skin_id)
if pos is None:
raise ValueError(f"Idol {skin_id} not found.")
# 确认
it.reset()
while btn_confirm := image.find(R.Common.ButtonConfirmNoIcon):
device.click(pos)
sleep(0.3)
device.click(btn_confirm)
it.wait()
@action('培育开始.编成翻页', screenshot_mode='manual-inherit')
def select_set(index: int):
@ -172,7 +137,7 @@ def resume_produce():
@action('执行培育', screenshot_mode='manual-inherit')
def do_produce(
idol: PIdol,
idol_skin_id: str,
mode: Literal['regular', 'pro'],
memory_set_index: Optional[int] = None
) -> bool:
@ -209,7 +174,7 @@ def do_produce(
device.click(image.expect_wait(R.InPurodyuusu.ButtonCancel))
return False
# 1. 选择 PIdol [screenshots/produce/select_p_idol.png]
select_idol(idol.to_title())
select_idol(idol_skin_id)
device.click(image.expect_wait(R.Common.ButtonNextNoIcon))
# 2. 选择支援卡 自动编成 [screenshots/produce/select_support_card.png]
ocr.expect_wait(contains('サポート'), rect=R.Produce.BoxStepIndicator)
@ -272,7 +237,7 @@ def do_produce(
def produce_task(
mode: Optional[Literal['regular', 'pro']] = None,
count: Optional[int] = None,
idols: Optional[list[PIdol]] = None,
idols: Optional[list[str]] = None,
memory_sets: Optional[list[int]] = None
):
"""
@ -280,7 +245,7 @@ def produce_task(
:param mode: 培育模式若为 None则从配置文件中读入
:param count: 培育次数若为 None则从配置文件中读入
:param idols: 要培育的偶像若为 None则从配置文件中读入
:param idols: 要培育的偶像IdolCardSkin.id若为 None则从配置文件中读入
"""
if not conf().produce.enabled:
logger.info('Produce is disabled.')
@ -310,7 +275,7 @@ def produce_task(
memory_set = next(memory_set_iterator, None)
logger.info(
f'Produce start with: '
f'idol: {idol.value}, mode: {mode}, memory_set: #{memory_set}'
f'idol: {idol}, mode: {mode}, memory_set: #{memory_set}'
)
if not do_produce(idol, mode, memory_set):
user.info('AP 不足', f'由于 AP 不足,跳过了 {count - i} 次培育。')
@ -340,13 +305,13 @@ if __name__ == '__main__':
conf().produce.enabled = True
conf().produce.mode = 'pro'
conf().produce.produce_count = 1
conf().produce.idols = [PIdol.月村手毬_アイヴイ]
conf().produce.idols = ['i_card-skin-hski-3-002']
conf().produce.memory_sets = [5]
conf().produce.auto_set_memory = False
# do_produce(PIdol.月村手毬_初声, 'pro', 5)
produce_task()
# a()
# select_idol(PIdol.藤田ことね_学園生活)
# select_idol()
# select_set(10)
# manual_context().begin()
# print(ocr.ocr(rect=R.Produce.BoxSetCountIndicator).squash().numbers())

View File

@ -0,0 +1,18 @@
import os
from typing import cast
from kotonebot.tasks import resources as res
CACHE = os.path.join('cache')
RESOURCE = cast(list[str], res.__path__)[0]
if not os.path.exists(CACHE):
os.makedirs(CACHE)
def cache(path: str) -> str:
p = os.path.join(CACHE, path)
os.makedirs(os.path.dirname(p), exist_ok=True)
return p
def resource(path: str) -> str:
return os.path.join(RESOURCE, path)

View File

@ -9,8 +9,11 @@ from typing import List, Dict, Tuple, Literal, Generator
import cv2
import gradio as gr
from kotonebot.backend.context import task_registry, ContextStackVars
from kotonebot.tasks.db import IdolCard
from kotonebot.backend.bot import KotoneBot
from kotonebot.config.manager import load_config, save_config
from kotonebot.config.base_config import UserConfig, BackendConfig
from kotonebot.backend.context import task_registry, ContextStackVars
from kotonebot.tasks.common import (
BaseConfig, APShopItems, CapsuleToysConfig, ClubRewardConfig, PurchaseConfig, ActivityFundsConfig,
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
@ -18,8 +21,6 @@ from kotonebot.tasks.common import (
RecommendCardDetectionMode, TraceConfig, StartGameConfig, UpgradeSupportCardConfig,
upgrade_config
)
from kotonebot.config.base_config import UserConfig, BackendConfig
from kotonebot.backend.bot import KotoneBot
# 初始化日志
os.makedirs('logs', exist_ok=True)
@ -330,7 +331,7 @@ class KotoneBotUI:
enabled=produce_enabled,
mode=produce_mode,
produce_count=produce_count,
idols=[PIdol[idol] for idol in produce_idols],
idols=produce_idols,
memory_sets=[int(i) for i in memory_sets],
auto_set_memory=auto_set_memory,
auto_set_support_card=auto_set_support,
@ -662,8 +663,13 @@ class KotoneBotUI:
info=ProduceConfig.model_fields['produce_count'].description
)
# 添加偶像选择
idol_choices = [idol.name for idol in PIdol]
selected_idols = [idol.name for idol in self.current_config.options.produce.idols]
idol_choices = []
for idol in IdolCard.all():
if idol.is_another:
idol_choices.append((f'{idol.name} 「{idol.another_name}', idol.skin_id))
else:
idol_choices.append((f'{idol.name}', idol.skin_id))
selected_idols = self.current_config.options.produce.idols
produce_idols = gr.Dropdown(
choices=idol_choices,
value=selected_idols,