koishi-plugin-rocom 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/lib/client.d.ts +53 -0
  2. package/lib/client.js +473 -0
  3. package/lib/commands/account.d.ts +5 -0
  4. package/lib/commands/account.js +205 -0
  5. package/lib/commands/admin.d.ts +2 -0
  6. package/lib/commands/admin.js +117 -0
  7. package/lib/commands/egg.d.ts +2 -0
  8. package/lib/commands/egg.js +196 -0
  9. package/lib/commands/merchant.d.ts +2 -0
  10. package/lib/commands/merchant.js +242 -0
  11. package/lib/commands/query.d.ts +2 -0
  12. package/lib/commands/query.js +1264 -0
  13. package/lib/commands/wiki.d.ts +2 -0
  14. package/lib/commands/wiki.js +11 -0
  15. package/lib/egg-service.d.ts +229 -0
  16. package/lib/egg-service.js +705 -0
  17. package/lib/index.d.ts +24 -0
  18. package/lib/index.js +3746 -0
  19. package/lib/render-templates/bind-list/index.html +51 -0
  20. package/lib/render-templates/bind-list/style.css +178 -0
  21. package/lib/render-templates/exchange-hall/css/_@astro-renderers.0KDkAyVb.css +1 -0
  22. package/lib/render-templates/exchange-hall/css/index.B3tv56V6.css +1 -0
  23. package/lib/render-templates/exchange-hall/css/index.D2LGPudy.css +1 -0
  24. package/lib/render-templates/exchange-hall/extracted.css +393 -0
  25. package/lib/render-templates/exchange-hall/index.html +99 -0
  26. package/lib/render-templates/exchange-hall/style.css +267 -0
  27. package/lib/render-templates/friendship/index.html +58 -0
  28. package/lib/render-templates/friendship/style.css +182 -0
  29. package/lib/render-templates/home/data/home_item_list.json +122 -0
  30. package/lib/render-templates/home/img/home_icon/100604.png +0 -0
  31. package/lib/render-templates/home/img/home_icon/100604_1.png +0 -0
  32. package/lib/render-templates/home/img/home_icon/100604_2.png +0 -0
  33. package/lib/render-templates/home/img/home_icon/100605.png +0 -0
  34. package/lib/render-templates/home/img/home_icon/100605_1.png +0 -0
  35. package/lib/render-templates/home/img/home_icon/100605_2.png +0 -0
  36. package/lib/render-templates/home/img/home_icon/100606.png +0 -0
  37. package/lib/render-templates/home/img/home_icon/100606_1.png +0 -0
  38. package/lib/render-templates/home/img/home_icon/100606_2.png +0 -0
  39. package/lib/render-templates/home/img/home_icon/100607.png +0 -0
  40. package/lib/render-templates/home/img/home_icon/100607_1.png +0 -0
  41. package/lib/render-templates/home/img/home_icon/100607_2.png +0 -0
  42. package/lib/render-templates/home/img/home_icon/100608.png +0 -0
  43. package/lib/render-templates/home/img/home_icon/100608_1.png +0 -0
  44. package/lib/render-templates/home/img/home_icon/100608_2.png +0 -0
  45. package/lib/render-templates/home/img/home_icon/100622.png +0 -0
  46. package/lib/render-templates/home/img/home_icon/100622_1.png +0 -0
  47. package/lib/render-templates/home/img/home_icon/100622_2.png +0 -0
  48. package/lib/render-templates/home/img/home_icon/100623.png +0 -0
  49. package/lib/render-templates/home/img/home_icon/100623_1.png +0 -0
  50. package/lib/render-templates/home/img/home_icon/100623_2.png +0 -0
  51. package/lib/render-templates/home/img/home_icon/100624.png +0 -0
  52. package/lib/render-templates/home/img/home_icon/100624_1.png +0 -0
  53. package/lib/render-templates/home/img/home_icon/100624_2.png +0 -0
  54. package/lib/render-templates/home/img/home_icon/100627.png +0 -0
  55. package/lib/render-templates/home/img/home_icon/100627_1.png +0 -0
  56. package/lib/render-templates/home/img/home_icon/100627_2.png +0 -0
  57. package/lib/render-templates/home/img/home_icon/100684.png +0 -0
  58. package/lib/render-templates/home/img/home_icon/100684_1.png +0 -0
  59. package/lib/render-templates/home/img/home_icon/100684_2.png +0 -0
  60. package/lib/render-templates/home/img/home_icon/100686.png +0 -0
  61. package/lib/render-templates/home/img/home_icon/100686_1.png +0 -0
  62. package/lib/render-templates/home/img/home_icon/100686_2.png +0 -0
  63. package/lib/render-templates/home/img/home_icon/100687.png +0 -0
  64. package/lib/render-templates/home/img/home_icon/100687_1.png +0 -0
  65. package/lib/render-templates/home/img/home_icon/100687_2.png +0 -0
  66. package/lib/render-templates/home/img/home_icon/100689.png +0 -0
  67. package/lib/render-templates/home/img/home_icon/100689_1.png +0 -0
  68. package/lib/render-templates/home/img/home_icon/100689_2.png +0 -0
  69. package/lib/render-templates/home/img/home_icon/100690.png +0 -0
  70. package/lib/render-templates/home/img/home_icon/100690_1.png +0 -0
  71. package/lib/render-templates/home/img/home_icon/100690_2.png +0 -0
  72. package/lib/render-templates/home/img/home_icon/100691.png +0 -0
  73. package/lib/render-templates/home/img/home_icon/100691_1.png +0 -0
  74. package/lib/render-templates/home/img/home_icon/100691_2.png +0 -0
  75. package/lib/render-templates/home/img/home_icon/100692.png +0 -0
  76. package/lib/render-templates/home/img/home_icon/100692_1.png +0 -0
  77. package/lib/render-templates/home/img/home_icon/100692_2.png +0 -0
  78. package/lib/render-templates/home/img/home_icon/100693.png +0 -0
  79. package/lib/render-templates/home/img/home_icon/100693_1.png +0 -0
  80. package/lib/render-templates/home/img/home_icon/100693_2.png +0 -0
  81. package/lib/render-templates/home/img/home_icon/100694.png +0 -0
  82. package/lib/render-templates/home/img/home_icon/100694_1.png +0 -0
  83. package/lib/render-templates/home/img/home_icon/100694_2.png +0 -0
  84. package/lib/render-templates/home/img/home_icon/100706.png +0 -0
  85. package/lib/render-templates/home/img/home_icon/100706_1.png +0 -0
  86. package/lib/render-templates/home/img/home_icon/100706_2.png +0 -0
  87. package/lib/render-templates/home/img/home_icon/100751.png +0 -0
  88. package/lib/render-templates/home/img/home_icon/100751_1.png +0 -0
  89. package/lib/render-templates/home/img/home_icon/100751_2.png +0 -0
  90. package/lib/render-templates/home/img/home_icon/100755.png +0 -0
  91. package/lib/render-templates/home/img/home_icon/100755_1.png +0 -0
  92. package/lib/render-templates/home/img/home_icon/100755_2.png +0 -0
  93. package/lib/render-templates/home/img/home_icon/100762.png +0 -0
  94. package/lib/render-templates/home/img/home_icon/100762_1.png +0 -0
  95. package/lib/render-templates/home/img/home_icon/100762_2.png +0 -0
  96. package/lib/render-templates/home/img/home_icon/100764.png +0 -0
  97. package/lib/render-templates/home/img/home_icon/100764_1.png +0 -0
  98. package/lib/render-templates/home/img/home_icon/100764_2.png +0 -0
  99. package/lib/render-templates/home/img/home_icon/100869.png +0 -0
  100. package/lib/render-templates/home/img/home_icon/100869_1.png +0 -0
  101. package/lib/render-templates/home/img/home_icon/100869_2.png +0 -0
  102. package/lib/render-templates/home/img/img_HomeVisit_Icon1.png +0 -0
  103. package/lib/render-templates/home/img/img_LevelReward_Bg2.png +0 -0
  104. package/lib/render-templates/home/index.html +139 -0
  105. package/lib/render-templates/home/style.css +537 -0
  106. package/lib/render-templates/ingame-shop/index.html +87 -0
  107. package/lib/render-templates/ingame-shop/style.css +220 -0
  108. package/lib/render-templates/inspect/index.html +47 -0
  109. package/lib/render-templates/inspect/style.css +149 -0
  110. package/lib/render-templates/lineup/index.html +77 -0
  111. package/lib/render-templates/lineup/style.css +255 -0
  112. package/lib/render-templates/lineup-detail/index.html +63 -0
  113. package/lib/render-templates/lineup-detail/style.css +218 -0
  114. package/lib/render-templates/menu/index.html +36 -0
  115. package/lib/render-templates/menu/style.css +126 -0
  116. package/lib/render-templates/package/index.html +115 -0
  117. package/lib/render-templates/package/style.css +352 -0
  118. package/lib/render-templates/personal-card/index.html +292 -0
  119. package/lib/render-templates/personal-card/style.css +2114 -0
  120. package/lib/render-templates/pet-wiki/index.html +118 -0
  121. package/lib/render-templates/pet-wiki/style.css +382 -0
  122. package/lib/render-templates/player-search/index.html +60 -0
  123. package/lib/render-templates/player-search/style.css +192 -0
  124. package/lib/render-templates/record/index.html +86 -0
  125. package/lib/render-templates/record/style.css +322 -0
  126. package/lib/render-templates/searcheggs/Pets.json +104328 -0
  127. package/lib/render-templates/searcheggs/candidates.html +52 -0
  128. package/lib/render-templates/searcheggs/eggs.py +599 -0
  129. package/lib/render-templates/searcheggs/index.html +198 -0
  130. package/lib/render-templates/searcheggs/pair.html +81 -0
  131. package/lib/render-templates/searcheggs/size.html +82 -0
  132. package/lib/render-templates/searcheggs/style.css +586 -0
  133. package/lib/render-templates/searcheggs/want.html +63 -0
  134. package/lib/render-templates/skill-wiki/index.html +68 -0
  135. package/lib/render-templates/skill-wiki/style.css +182 -0
  136. package/lib/render-templates/student/index.html +95 -0
  137. package/lib/render-templates/student/style.css +255 -0
  138. package/lib/render-templates/student-perks/index.html +78 -0
  139. package/lib/render-templates/student-perks/style.css +238 -0
  140. package/lib/render-templates/student-state/index.html +52 -0
  141. package/lib/render-templates/student-state/style.css +157 -0
  142. package/lib/render-templates/yuanxing-shangren/index.html +371 -0
  143. package/lib/render-templates/yuanxing-shangren/style.css +371 -0
  144. package/lib/render.d.ts +11 -0
  145. package/lib/render.js +226 -0
  146. package/lib/role-token.d.ts +27 -0
  147. package/lib/role-token.js +137 -0
  148. package/lib/send-image.d.ts +3 -0
  149. package/lib/send-image.js +135 -0
  150. package/lib/subscription-send.d.ts +8 -0
  151. package/lib/subscription-send.js +48 -0
  152. package/lib/types.d.ts +32 -0
  153. package/lib/types.js +2 -0
  154. package/lib/user.d.ts +67 -0
  155. package/lib/user.js +176 -0
  156. package/package.json +58 -0
  157. package/readme.md +575 -0
@@ -0,0 +1,52 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="stylesheet" href="{{_res_path}}render/searcheggs/style.css">
6
+ <style>
7
+ .search-card-list { display: grid; gap: 14px; margin-top: 22px; }
8
+ .search-card-item { background: rgba(255,255,255,0.94); border-radius: 18px; padding: 18px 20px; box-shadow: 0 10px 24px rgba(40, 31, 23, 0.08); }
9
+ .search-card-title { display: flex; justify-content: space-between; gap: 12px; font-size: 28px; font-weight: 700; color: #4d3624; }
10
+ .search-card-meta { margin-top: 10px; color: #75563d; font-size: 18px; line-height: 1.7; }
11
+ .search-empty { text-align: center; font-size: 24px; color: #6f5845; padding: 48px 24px; }
12
+ .pagination-footer { margin-top: 22px; display: flex; justify-content: space-between; gap: 14px; color: #8c7a61; font-size: 14px; }
13
+ .pagination-hint { max-width: 70%; }
14
+ .copyright { white-space: nowrap; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div class="searcheggs-cont page-section-main">
19
+ <div class="header-card">
20
+ <div class="header-main">
21
+ <div class="header-title">查蛋候选结果</div>
22
+ <div class="header-subtitle">关键词「{{keyword}}」命中 {{count}} 个候选,请使用更精确的名称</div>
23
+ </div>
24
+ </div>
25
+
26
+ {{if candidates && candidates.length}}
27
+ <div class="search-card-list">
28
+ {{each candidates item}}
29
+ <div class="search-card-item">
30
+ <div class="search-card-title">
31
+ <span>{{item.name}}</span>
32
+ <span>#{{item.id}}</span>
33
+ </div>
34
+ <div class="search-card-meta">
35
+ 属性:{{item.type_label}}<br>
36
+ 蛋组:{{item.egg_groups_label}}<br>
37
+ 身高:{{item.height_label}} | 体重:{{item.weight_label}}
38
+ </div>
39
+ </div>
40
+ {{/each}}
41
+ </div>
42
+ {{else}}
43
+ <div class="search-empty">没有可展示的候选结果</div>
44
+ {{/if}}
45
+
46
+ <div class="pagination-footer">
47
+ <span class="pagination-hint">{{commandHint}}</span>
48
+ <span class="copyright">{{copyright}}</span>
49
+ </div>
50
+ </div>
51
+ </body>
52
+ </html>
@@ -0,0 +1,599 @@
1
+ """
2
+ 洛克王国查蛋模块 (自包含)
3
+
4
+ 数据、逻辑、渲染模板均在 render/searcheggs/ 下,
5
+ 外部 main.py 仅做指令路由的薄调用层。
6
+ """
7
+
8
+ import os
9
+ import json
10
+ from typing import Dict, List, Optional
11
+
12
+ try:
13
+ from astrbot.api import logger
14
+ except ImportError:
15
+ import logging
16
+ logger = logging.getLogger(__name__)
17
+ if not logger.handlers:
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))
20
+ logger.addHandler(handler)
21
+ logger.setLevel(logging.INFO)
22
+
23
+ # ── 蛋组元数据 ──────────────────────────────────────────────
24
+
25
+ EGG_GROUP_META = {
26
+ 1: {"label": "未发现", "desc": "不能和任何精灵生蛋,多用于传说中的精灵"},
27
+ 2: {"label": "怪兽", "desc": "像怪兽一样,或者比较野性的动物"},
28
+ 3: {"label": "两栖", "desc": "两栖动物和水边生活的多栖动物"},
29
+ 4: {"label": "虫", "desc": "看起来像虫子的精灵"},
30
+ 5: {"label": "飞行", "desc": "会飞的精灵"},
31
+ 6: {"label": "陆上", "desc": "生活在陆地上的精灵"},
32
+ 7: {"label": "妖精", "desc": "可爱的小动物,以及神话中的精灵"},
33
+ 8: {"label": "植物", "desc": "看起来像植物的精灵"},
34
+ 9: {"label": "人型", "desc": "看起来像人的精灵"},
35
+ 10: {"label": "软体", "desc": "看起来软软的精灵,圆形多为软体动物"},
36
+ 11: {"label": "矿物", "desc": "身体由矿物组成的精灵"},
37
+ 12: {"label": "不定形", "desc": "没有固定形态的精灵,包括水、火、灵魂、能量"},
38
+ 13: {"label": "鱼", "desc": "看起来像鱼的精灵"},
39
+ 14: {"label": "龙", "desc": "看起来像龙的精灵"},
40
+ 15: {"label": "机械", "desc": "身体由机械组成的精灵"},
41
+ }
42
+
43
+
44
+ def get_egg_group_label(group_id: int) -> str:
45
+ meta = EGG_GROUP_META.get(group_id)
46
+ return meta["label"] if meta else f"蛋组{group_id}"
47
+
48
+
49
+ def format_egg_groups(group_ids: List[int]) -> str:
50
+ if not group_ids:
51
+ return "暂无蛋组数据"
52
+ return " / ".join(get_egg_group_label(gid) for gid in group_ids)
53
+
54
+
55
+ # ── 搜索结果类型 ─────────────────────────────────────────────
56
+
57
+ class SearchResult:
58
+ """搜索结果封装,区分精确/模糊/多候选"""
59
+ EXACT = "exact"
60
+ FUZZY = "fuzzy"
61
+ MULTI = "multi"
62
+ NOT_FOUND = "not_found"
63
+
64
+ def __init__(self, match_type: str, pet=None, candidates=None):
65
+ self.match_type = match_type
66
+ self.pet = pet # 单个匹配结果
67
+ self.candidates: List[dict] = candidates or [] # 多候选
68
+
69
+
70
+ # ── 查蛋引擎 ─────────────────────────────────────────────────
71
+
72
+ class EggSearcher:
73
+ """蛋组查询引擎,数据自包含在 render/searcheggs/ 下"""
74
+
75
+ def __init__(self, data_dir: str = None):
76
+ """
77
+ data_dir: render/searcheggs/ 目录路径。
78
+ 若不传则自动取本文件所在目录。
79
+ """
80
+ if data_dir is None:
81
+ data_dir = os.path.dirname(os.path.abspath(__file__))
82
+ self._data_dir = data_dir
83
+ self._pets: List[dict] = []
84
+ self._by_id: Dict[int, dict] = {}
85
+ self._by_zh: Dict[str, dict] = {} # 中文名 → pet (精确)
86
+ self._by_en: Dict[str, dict] = {} # 英文名小写 → pet
87
+ self._load()
88
+
89
+ def _load(self):
90
+ path = os.path.join(self._data_dir, "Pets.json")
91
+ if not os.path.exists(path):
92
+ logger.error(f"[Eggs] Pets.json 不存在: {path}")
93
+ return
94
+ try:
95
+ with open(path, "r", encoding="utf-8") as f:
96
+ raw = json.load(f)
97
+ self._pets = raw if isinstance(raw, list) else []
98
+ for p in self._pets:
99
+ self._by_id[p["id"]] = p
100
+ zh = p.get("localized", {}).get("zh", {}).get("name", "")
101
+ if zh:
102
+ self._by_zh[zh] = p
103
+ en = p.get("name", "").lower()
104
+ if en:
105
+ self._by_en[en] = p
106
+ logger.info(f"[Eggs] 加载 {len(self._pets)} 只精灵")
107
+ except Exception as e:
108
+ logger.error(f"[Eggs] 加载失败: {e}")
109
+
110
+ # ── 按身高/体重反查 ──
111
+
112
+ def search_by_size(self, height: float = None, weight: float = None) -> dict:
113
+ """
114
+ 通过身高(cm)或体重(kg)反查精灵。
115
+ 返回结构:
116
+ {
117
+ "perfect": [精确匹配的精灵列表],
118
+ "range": [范围匹配的精灵列表(带容差)],
119
+ }
120
+ """
121
+ perfect_results = []
122
+ range_results = []
123
+
124
+ for p in self._pets:
125
+ br = p.get("breeding") or {}
126
+ h_lo = br.get("height_low")
127
+ h_hi = br.get("height_high")
128
+ w_lo = br.get("weight_low")
129
+ w_hi = br.get("weight_high")
130
+
131
+ height_match_type = None # None=不查, "perfect"=完美, "range"=范围
132
+ weight_match_type = None
133
+
134
+ # 身高匹配判定
135
+ if height is not None:
136
+ if h_lo is not None and h_hi is not None:
137
+ # 完美匹配:输入值在 [height_low, height_high] 区间内
138
+ if h_lo <= height <= h_hi:
139
+ height_match_type = "perfect"
140
+ else:
141
+ # 范围匹配:容差 15%
142
+ h_min = h_lo * 0.85
143
+ h_max = h_hi * 1.15
144
+ if h_min <= height <= h_max:
145
+ height_match_type = "range"
146
+ else:
147
+ height_match_type = "none"
148
+ else:
149
+ height_match_type = "none"
150
+
151
+ # 体重匹配判定(Pets.json 中 weight 单位是克,用户输入 kg)
152
+ if weight is not None:
153
+ if w_lo is not None and w_hi is not None:
154
+ w_kg_lo = w_lo / 1000
155
+ w_kg_hi = w_hi / 1000
156
+ # 完美匹配:输入值在 [weight_low, weight_high] 区间内(kg)
157
+ if w_kg_lo <= weight <= w_kg_hi:
158
+ weight_match_type = "perfect"
159
+ else:
160
+ # 范围匹配:容差 15%
161
+ w_min = w_kg_lo * 0.85
162
+ w_max = w_kg_hi * 1.15
163
+ if w_min <= weight <= w_max:
164
+ weight_match_type = "range"
165
+ else:
166
+ weight_match_type = "none"
167
+ else:
168
+ weight_match_type = "none"
169
+
170
+ # 综合判定
171
+ if height is not None and weight is not None:
172
+ # 双条件:两者都完美才算完美,任一范围就算范围
173
+ if height_match_type == "perfect" and weight_match_type == "perfect":
174
+ perfect_results.append(p)
175
+ elif height_match_type != "none" and weight_match_type != "none":
176
+ range_results.append(p)
177
+ elif height is not None:
178
+ if height_match_type == "perfect":
179
+ perfect_results.append(p)
180
+ elif height_match_type == "range":
181
+ range_results.append(p)
182
+ elif weight is not None:
183
+ if weight_match_type == "perfect":
184
+ perfect_results.append(p)
185
+ elif weight_match_type == "range":
186
+ range_results.append(p)
187
+
188
+ return {
189
+ "perfect": perfect_results[:20],
190
+ "range": range_results[:20],
191
+ }
192
+
193
+ def build_size_search_text(self, height: float = None, weight: float = None, results: dict = None) -> str:
194
+ """构建身高/体重反查结果文本(区分完美匹配和范围匹配)"""
195
+ cond = []
196
+ if height is not None:
197
+ cond.append(f"身高={height}cm")
198
+ if weight is not None:
199
+ cond.append(f"体重={weight}kg")
200
+ cond_str = " + ".join(cond)
201
+
202
+ if not results or (not results["perfect"] and not results["range"]):
203
+ return f"❌ 未找到符合 {cond_str} 的精灵。"
204
+
205
+ lines = []
206
+
207
+ # 完美匹配
208
+ if results["perfect"]:
209
+ lines.append(f"✅ 完美匹配 {cond_str} 的精灵(共 {len(results['perfect'])} 只):")
210
+ for i, p in enumerate(results["perfect"][:10], 1):
211
+ zh = self._name(p)
212
+ br = p.get("breeding") or {}
213
+ h_str = self._fmt_range(br.get("height_low"), br.get("height_high"), "cm")
214
+ w_str = self._fmt_range(self._wt(br.get("weight_low")), self._wt(br.get("weight_high")), "kg")
215
+ egs = format_egg_groups(self.get_egg_groups(p))
216
+ lines.append(f" {i}. {zh} (#{p['id']}) — {h_str} / {w_str} · {egs}")
217
+ if len(results["perfect"]) > 10:
218
+ lines.append(f" ... 还有 {len(results['perfect'])-10} 个结果")
219
+
220
+ # 范围匹配
221
+ if results["range"]:
222
+ if lines:
223
+ lines.append("")
224
+ lines.append(f"🔍 范围匹配 {cond_str} 的精灵(共 {len(results['range'])} 只,容差±15%):")
225
+ for i, p in enumerate(results["range"][:10], 1):
226
+ zh = self._name(p)
227
+ br = p.get("breeding") or {}
228
+ h_str = self._fmt_range(br.get("height_low"), br.get("height_high"), "cm")
229
+ w_str = self._fmt_range(self._wt(br.get("weight_low")), self._wt(br.get("weight_high")), "kg")
230
+ egs = format_egg_groups(self.get_egg_groups(p))
231
+ lines.append(f" {i}. {zh} (#{p['id']}) — {h_str} / {w_str} · {egs}")
232
+ if len(results["range"]) > 10:
233
+ lines.append(f" ... 还有 {len(results['range'])-10} 个结果")
234
+
235
+ lines.append("\n💡 /洛克查蛋 <精灵名> 查看详细蛋组信息")
236
+ return "\n".join(lines)
237
+
238
+ # ── 配种结果查询 ──
239
+
240
+ def get_breeding_parents(self, pet: dict) -> List[dict]:
241
+ """
242
+ 想要孵出指定精灵,需要哪些父母组合?
243
+ 规则:母体决定孵出结果,所以母体必须是目标精灵(或其进化链最低形态)。
244
+ 返回所有可作为父体的精灵列表。
245
+ """
246
+ egg_groups = set(self.get_egg_groups(pet))
247
+ if not egg_groups or 1 in egg_groups:
248
+ return []
249
+ fathers = []
250
+ for o in self._pets:
251
+ if o["id"] == pet["id"]:
252
+ continue
253
+ og = set(self.get_egg_groups(o))
254
+ if not og or 1 in og:
255
+ continue
256
+ if egg_groups & og:
257
+ fathers.append(o)
258
+ return fathers
259
+
260
+ def build_want_pet_text(self, pet: dict) -> str:
261
+ """构建「想要某精灵需要怎么配」的文本"""
262
+ zh = self._name(pet)
263
+ egs = self.get_egg_groups(pet)
264
+ bp = pet.get("breeding_profile") or {}
265
+ female_rate = bp.get("female_rate")
266
+ male_rate = bp.get("male_rate")
267
+
268
+ lines = [f"🥚 想要孵出「{zh}」:"]
269
+ lines.append(f"蛋组:{format_egg_groups(egs)}")
270
+
271
+ if 1 in egs:
272
+ lines.append("⚠️ 该精灵属于「未发现」蛋组,无法通过配种获得。")
273
+ return "\n".join(lines)
274
+
275
+ lines.append(f"\n📌 母体必须是「{zh}」(孵蛋结果跟随母体)")
276
+ if female_rate is not None:
277
+ lines.append(f" 母体概率:{female_rate}%")
278
+ if female_rate <= 0:
279
+ lines.append(f" ⚠️ 该精灵雌性概率为 0%,可能无法作为母体!")
280
+
281
+ fathers = self.get_breeding_parents(pet)
282
+ if fathers:
283
+ lines.append(f"\n🔗 可选父体(共 {len(fathers)} 只,需雄性):")
284
+ for i, f in enumerate(fathers[:15], 1):
285
+ f_bp = f.get("breeding_profile") or {}
286
+ f_male = f_bp.get("male_rate")
287
+ male_hint = f" (♂{f_male}%)" if f_male is not None else ""
288
+ lines.append(f" {i}. {self._name(f)}{male_hint} — {format_egg_groups(self.get_egg_groups(f))}")
289
+ if len(fathers) > 15:
290
+ lines.append(f" ... 还有 {len(fathers)-15} 只")
291
+ else:
292
+ lines.append("\n❌ 未找到可配种的父体精灵。")
293
+
294
+ lines.append("\n💡 /洛克配种 <父体> <母体> 查看详细配种结果")
295
+ return "\n".join(lines)
296
+
297
+ # ── 搜索 ──
298
+
299
+ def search(self, keyword: str) -> SearchResult:
300
+ """
301
+ 智能搜索:
302
+ 1. 精确匹配中文名 / ID / 英文名
303
+ 2. 单一模糊命中 → 视为精确
304
+ 3. 多个模糊命中 → 返回候选列表
305
+ 4. 无命中 → NOT_FOUND
306
+ """
307
+ kw = keyword.strip()
308
+ if not kw:
309
+ return SearchResult(SearchResult.NOT_FOUND)
310
+
311
+ # 1) 精确:中文名
312
+ if kw in self._by_zh:
313
+ return SearchResult(SearchResult.EXACT, pet=self._by_zh[kw])
314
+
315
+ # 2) 精确:ID
316
+ try:
317
+ pid = int(kw)
318
+ if pid in self._by_id:
319
+ return SearchResult(SearchResult.EXACT, pet=self._by_id[pid])
320
+ except ValueError:
321
+ pass
322
+
323
+ # 3) 精确:英文名(不区分大小写)
324
+ if kw.lower() in self._by_en:
325
+ return SearchResult(SearchResult.EXACT, pet=self._by_en[kw.lower()])
326
+
327
+ # 4) 模糊匹配
328
+ kw_lower = kw.lower()
329
+ hits = []
330
+ for p in self._pets:
331
+ zh = p.get("localized", {}).get("zh", {}).get("name", "")
332
+ en = p.get("name", "")
333
+ if kw_lower in zh.lower() or kw_lower in en.lower():
334
+ hits.append(p)
335
+
336
+ if len(hits) == 1:
337
+ return SearchResult(SearchResult.FUZZY, pet=hits[0])
338
+ if len(hits) > 1:
339
+ return SearchResult(SearchResult.MULTI, candidates=hits[:20])
340
+
341
+ return SearchResult(SearchResult.NOT_FOUND)
342
+
343
+ # ── 蛋组 / 配种 ──
344
+
345
+ def get_egg_groups(self, pet: dict) -> List[int]:
346
+ bp = pet.get("breeding_profile")
347
+ return bp.get("egg_groups", []) if bp else []
348
+
349
+ def get_compatible_pets(self, pet: dict) -> List[dict]:
350
+ groups = set(self.get_egg_groups(pet))
351
+ if not groups or 1 in groups:
352
+ return []
353
+ out = []
354
+ for o in self._pets:
355
+ if o["id"] == pet["id"]:
356
+ continue
357
+ og = set(self.get_egg_groups(o))
358
+ if not og or 1 in og:
359
+ continue
360
+ if groups & og:
361
+ out.append(o)
362
+ return out
363
+
364
+ def evaluate_pair(self, a: dict, b: dict) -> dict:
365
+ ga, gb = set(self.get_egg_groups(a)), set(self.get_egg_groups(b))
366
+ shared = sorted(ga & gb)
367
+ reasons = []
368
+ if not ga:
369
+ reasons.append(f"{self._name(a)} 暂无蛋组数据")
370
+ if not gb:
371
+ reasons.append(f"{self._name(b)} 暂无蛋组数据")
372
+ if 1 in ga:
373
+ reasons.append(f"{self._name(a)} 属于「未发现」蛋组")
374
+ if 1 in gb:
375
+ reasons.append(f"{self._name(b)} 属于「未发现」蛋组")
376
+ if not shared and not reasons:
377
+ reasons.append("蛋组不相同,无法配种")
378
+ br = a.get("breeding") or {}
379
+ return {
380
+ "compatible": not reasons and len(shared) > 0,
381
+ "reasons": reasons,
382
+ "shared_egg_groups": shared,
383
+ "shared_egg_group_labels": [get_egg_group_label(g) for g in shared],
384
+ "hatch_label": self._fmt_dur(br.get("hatch_data")),
385
+ "weight_label": self._fmt_range(self._wt(br.get("weight_low")), self._wt(br.get("weight_high")), "kg"),
386
+ "height_label": self._fmt_range(br.get("height_low"), br.get("height_high"), "cm"),
387
+ }
388
+
389
+ # ── 构建渲染数据 ──
390
+
391
+ def build_search_data(self, pet: dict) -> dict:
392
+ egs = self.get_egg_groups(pet)
393
+ compat = self.get_compatible_pets(pet)
394
+ gmap: Dict[int, List[dict]] = {gid: [] for gid in egs if gid != 1}
395
+ for c in compat:
396
+ for gid in egs:
397
+ if gid in self.get_egg_groups(c) and gid != 1:
398
+ gmap.setdefault(gid, []).append(c)
399
+ sections = []
400
+ for gid in egs:
401
+ meta = EGG_GROUP_META.get(gid, {})
402
+ members = gmap.get(gid, [])
403
+ sections.append({
404
+ "id": gid,
405
+ "label": meta.get("label", f"蛋组{gid}"),
406
+ "desc": meta.get("desc", ""),
407
+ "count": len(members),
408
+ "members": [{"name": self._name(m), "id": m["id"],
409
+ "type_label": self._type(m),
410
+ "egg_groups_label": format_egg_groups(self.get_egg_groups(m))}
411
+ for m in members[:30]],
412
+ "has_more": len(members) > 30,
413
+ "total": len(members),
414
+ })
415
+ br = pet.get("breeding") or {}
416
+ bp = pet.get("breeding_profile") or {}
417
+
418
+ # 蛋详细数据
419
+ egg_details = self._build_egg_details(br)
420
+
421
+ return {
422
+ "pet_name": self._name(pet), "pet_id": pet["id"],
423
+ "pet_icon": self._pet_icon_url(pet["id"]),
424
+ "pet_image": self._pet_image_url(pet["id"]),
425
+ "type_label": self._type(pet),
426
+ "egg_groups_label": format_egg_groups(egs),
427
+ "egg_groups": egs,
428
+ "egg_group_labels": {gid: get_egg_group_label(gid) for gid in egs},
429
+ "male_rate": bp.get("male_rate"), "female_rate": bp.get("female_rate"),
430
+ "hatch_label": self._fmt_dur(br.get("hatch_data")),
431
+ "weight_label": self._fmt_range(self._wt(br.get("weight_low")), self._wt(br.get("weight_high")), "kg"),
432
+ "height_label": self._fmt_range(br.get("height_low"), br.get("height_high"), "cm"),
433
+ "total_compatible": len(compat),
434
+ "is_undiscovered": 1 in egs,
435
+ "egg_group_sections": sections,
436
+ "total_stats": sum(pet.get(k, 0) for k in
437
+ ["base_hp","base_phy_atk","base_mag_atk","base_phy_def","base_mag_def","base_spd"]),
438
+ "egg_details": egg_details,
439
+ }
440
+
441
+ def _build_egg_details(self, breeding: dict) -> dict:
442
+ """构建蛋详细数据"""
443
+ if not breeding:
444
+ return {"has_data": False}
445
+
446
+ # 基础异色概率
447
+ base_prob_array = breeding.get("egg_base_glass_prob_array")
448
+ if base_prob_array and len(base_prob_array) == 2:
449
+ base_prob = base_prob_array[0] / base_prob_array[1]
450
+ base_prob_pct = base_prob * 100
451
+ base_prob_str = f"{base_prob_array[0]}/{base_prob_array[1]}"
452
+ else:
453
+ base_prob = None
454
+ base_prob_pct = None
455
+ base_prob_str = "暂无数据"
456
+
457
+ # 额外异色概率
458
+ add_prob_array = breeding.get("egg_add_glass_prob_array")
459
+ if add_prob_array and len(add_prob_array) == 2:
460
+ add_prob = add_prob_array[0] / add_prob_array[1]
461
+ add_prob_pct = add_prob * 100
462
+ add_prob_str = f"{add_prob_array[0]}/{add_prob_array[1]}"
463
+ else:
464
+ add_prob = None
465
+ add_prob_pct = None
466
+ add_prob_str = "暂无数据"
467
+
468
+ # 异色概率说明
469
+ is_contact_add_glass = breeding.get("is_contact_add_glass_prob")
470
+ is_contact_add_shining = breeding.get("is_contact_add_shining_prob")
471
+
472
+ # 珍稀蛋类型
473
+ precious_egg_type = breeding.get("precious_egg_type")
474
+ precious_egg_label = self._get_precious_egg_label(precious_egg_type)
475
+
476
+ # 变体数据
477
+ variants = breeding.get("variants") or []
478
+ variant_list = []
479
+ for v in variants:
480
+ variant_info = {
481
+ "id": v.get("id"),
482
+ "name": v.get("name", ""),
483
+ "hatch_label": self._fmt_dur(v.get("hatch_data")),
484
+ "weight_label": self._fmt_range(self._wt(v.get("weight_low")), self._wt(v.get("weight_high")), "kg"),
485
+ "height_label": self._fmt_range(v.get("height_low"), v.get("height_high"), "cm"),
486
+ "precious_egg_type": v.get("precious_egg_type"),
487
+ "precious_egg_label": self._get_precious_egg_label(v.get("precious_egg_type")),
488
+ }
489
+ # 变体的异色概率
490
+ v_base = v.get("egg_base_glass_prob_array")
491
+ if v_base and len(v_base) == 2:
492
+ variant_info["base_prob_str"] = f"{v_base[0]}/{v_base[1]}"
493
+ else:
494
+ variant_info["base_prob_str"] = "暂无"
495
+ variant_list.append(variant_info)
496
+
497
+ return {
498
+ "has_data": True,
499
+ "base_prob_str": base_prob_str,
500
+ "base_prob_pct": base_prob_pct,
501
+ "add_prob_str": add_prob_str,
502
+ "add_prob_pct": add_prob_pct,
503
+ "is_contact_add_glass": is_contact_add_glass,
504
+ "is_contact_add_shining": is_contact_add_shining,
505
+ "precious_egg_type": precious_egg_type,
506
+ "precious_egg_label": precious_egg_label,
507
+ "variants": variant_list,
508
+ "variant_count": len(variant_list),
509
+ }
510
+
511
+ def _get_precious_egg_label(self, egg_type) -> str:
512
+ """获取珍稀蛋类型标签"""
513
+ if egg_type is None:
514
+ return "普通蛋"
515
+ precious_map = {
516
+ 1: "迪莫蛋",
517
+ 2: "星辰蛋",
518
+ 3: "彩虹蛋",
519
+ 4: "梦幻蛋",
520
+ 5: "传说蛋",
521
+ 6: "神秘蛋",
522
+ 7: "特殊蛋",
523
+ }
524
+ return precious_map.get(egg_type, f"珍稀蛋(类型{egg_type})")
525
+
526
+ def build_pair_data(self, a: dict, b: dict) -> dict:
527
+ ev = self.evaluate_pair(a, b)
528
+ return {
529
+ "mother": {"name": self._name(a), "id": a["id"],
530
+ "type_label": self._type(a),
531
+ "egg_groups_label": format_egg_groups(self.get_egg_groups(a))},
532
+ "father": {"name": self._name(b), "id": b["id"],
533
+ "type_label": self._type(b),
534
+ "egg_groups_label": format_egg_groups(self.get_egg_groups(b))},
535
+ **ev,
536
+ }
537
+
538
+ def build_candidates_text(self, keyword: str, candidates: List[dict]) -> str:
539
+ """构建多候选的友好提示文本"""
540
+ lines = [f"🔍 「{keyword}」匹配到 {len(candidates)} 只精灵,请精确输入:"]
541
+ for i, p in enumerate(candidates[:10], 1):
542
+ zh = self._name(p)
543
+ egs = format_egg_groups(self.get_egg_groups(p))
544
+ lines.append(f" {i}. {zh} (#{p['id']}) — {self._type(p)} · {egs}")
545
+ if len(candidates) > 10:
546
+ lines.append(f" ... 还有 {len(candidates)-10} 个结果")
547
+ lines.append("\n💡 请使用精确名称重新查询,如:/洛克查蛋 喵喵")
548
+ return "\n".join(lines)
549
+
550
+ # ── 工具 ──
551
+
552
+ def _name(self, p: dict) -> str:
553
+ return p.get("localized", {}).get("zh", {}).get("name", p.get("name", "???"))
554
+
555
+ def _type(self, p: dict) -> str:
556
+ parts = []
557
+ mt = p.get("main_type", {}).get("localized", {}).get("zh", "")
558
+ if mt: parts.append(mt)
559
+ st = (p.get("sub_type") or {}).get("localized", {}).get("zh", "")
560
+ if st: parts.append(st)
561
+ return " / ".join(parts) or "未知"
562
+
563
+ @staticmethod
564
+ def _fmt_dur(s) -> str:
565
+ if not s or s <= 0: return "暂无数据"
566
+ if s % 86400 == 0: return f"{s//86400} 天"
567
+ h = s / 3600
568
+ return f"{int(h)} 小时" if h == int(h) else f"{h:.1f} 小时"
569
+
570
+ @staticmethod
571
+ def _wt(v) -> Optional[float]:
572
+ return round(v/1000, 1) if v is not None else None
573
+
574
+ @staticmethod
575
+ def _fmt_range(lo, hi, u: str) -> str:
576
+ if lo is None and hi is None: return "暂无数据"
577
+ if lo is not None and hi is not None:
578
+ return f"{lo}{u}" if lo == hi else f"{lo}-{hi}{u}"
579
+ return f"{lo or hi}{u}"
580
+
581
+ @staticmethod
582
+ def _asset_pet_id(pet_id) -> Optional[int]:
583
+ try:
584
+ numeric_id = int(pet_id)
585
+ except (TypeError, ValueError):
586
+ return None
587
+ return numeric_id if numeric_id >= 3000 else numeric_id + 3000
588
+
589
+ def _pet_icon_url(self, pet_id) -> str:
590
+ asset_id = self._asset_pet_id(pet_id)
591
+ if asset_id is None:
592
+ return "{{_res_path}}img/roco_icon.png"
593
+ return f"https://game.gtimg.cn/images/rocom/rocodata/jingling/{asset_id}/icon.png"
594
+
595
+ def _pet_image_url(self, pet_id) -> str:
596
+ asset_id = self._asset_pet_id(pet_id)
597
+ if asset_id is None:
598
+ return "{{_res_path}}img/roco_icon.png"
599
+ return f"https://game.gtimg.cn/images/rocom/rocodata/jingling/{asset_id}/image.png"