新增查看炼金配方的功能

This commit is contained in:
2025-11-11 00:06:56 +08:00
parent 6cda620650
commit cb9aed34ef
3 changed files with 281 additions and 5 deletions

View File

@@ -14,7 +14,7 @@ alwaysApply: true
你必须完全理解这个项目, 并明白文件夹PWF里的文件你没有权力更改
你必须熟读 WPSAPI.py 和 plugin_interface.py 才有可能不犯错误
你必须熟读 @WPSAPI.py 和 @plugin_interface.py 才有可能不犯错误
### 元指令:模式声明要求

View File

@@ -0,0 +1,38 @@
# 背景
文件名: 2025-11-10_3
创建于: 2025-11-10_23:22:58
创建者: ASUS
主分支: main
任务分支: 未创建
Yolo模式: Off
# 任务描述
为炼金系统添加新的查询插件类,支持指令`炼金配方 <物品id|物品名>`以查看与物品相关的炼金配方。
# 项目概览
当前炼金系统由`WPSAlchemyGame`插件提供积分炼金和物品炼金功能,包含配方注册、运势修正、物品消耗与奖励结算。系统依赖`WPSBackpackSystem``WPSFortuneSystem``WPSStoreSystem`等模块,配方信息保存在`_recipes`字典中。
# 分析
# 当前炼金逻辑由 `WPSAlchemyGame` 维护 `_recipes` 字典保存配方,键为排序后三材料三元组,仅在炼金过程内部消费;缺少公开查询接口。
# 诸如菜园系统通过 `register_recipe` 与炼金模块交互,说明炼金插件是配方注册中心;新增功能需要扩展其对外 API 而非直接访问私有属性。
# 背包系统支持根据 ID/名称解析物品定义,为展示配方详情提供了名称、稀有度等信息,可在查询插件中调用。
# 目前不存在“炼金配方”指令插件,需创建新插件并注册命令,使聊天指令能够调用查询逻辑。
# 提议的解决方案
# 在 `WPSAlchemyGame` 中新增只读接口,支持基于物品 ID 返回其作为材料及作为成功/失败产物的配方列表;必要时可维护索引或在查询时遍历 `_recipes`。
# 新建 `WPSAlchemyRecipeLookup`(名称待定)插件,依赖炼金与背包系统,在 `wake_up` 中注册 `炼金配方` 命令,并在 `callback` 中完成参数解析、配方查询、结果排序与 Markdown 渲染。
# 输出两个有序列表,分别展示目标物品参与的配方(作为材料)以及目标物品对应的产物/失败产物信息;同一物品若兼具多种角色则在两个列表中分别呈现。
# 当前执行步骤:"2. 创建任务文件"
# 任务进度
[2025-11-10_23:51:23]
- 已修改: Plugins/WPSAlchemyGame.py Plugins/WPSAlchemyRecipeLookup.py
- 更改: 新增炼金配方查询索引接口与查询插件草案,实现“炼金配方”指令基础逻辑
- 原因: 支持查询炼金配方需求,避免直接访问私有数据结构
- 阻碍因素: 暂无
- 状态: 未确认
# 最终审查
待补充

View File

@@ -1,8 +1,9 @@
from __future__ import annotations
import random
from collections import defaultdict, Counter
from dataclasses import dataclass
from typing import Dict, List, Optional, Sequence, Tuple, override
from typing import Dict, List, Optional, Sequence, Set, Tuple, override
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
@@ -55,6 +56,9 @@ class WPSAlchemyGame(WPSAPI):
def __init__(self) -> None:
super().__init__()
self._recipes: Dict[Tuple[str, str, str], AlchemyRecipe] = {}
self._material_index: Dict[str, Set[Tuple[str, str, str]]] = defaultdict(set)
self._success_index: Dict[str, Set[Tuple[str, str, str]]] = defaultdict(set)
self._fail_index: Dict[str, Set[Tuple[str, str, str]]] = defaultdict(set)
self._fortune_coeff = FORTUNE_COEFF
@override
@@ -138,18 +142,81 @@ class WPSAlchemyGame(WPSAPI):
clamped_rate = clamp01(base_success_rate)
if clamped_rate != base_success_rate:
raise ValueError("配方成功率必须位于 0~1 之间")
sorted_key = tuple(sorted_materials)
existing = self._recipes.get(sorted_key)
if existing:
self._unindex_recipe(sorted_key, existing)
recipe = AlchemyRecipe(
materials=sorted_materials,
materials=sorted_key,
success_item_id=success_item_id.strip(),
fail_item_id=fail_item_id.strip() or self.ASH_ITEM_ID,
base_success_rate=base_success_rate,
)
self._recipes[sorted_materials] = recipe
self._recipes[sorted_key] = recipe
self._index_recipe(sorted_key, recipe)
logger.Log(
"Info",
f"{ConsoleFrontColor.CYAN}已注册炼金配方 {sorted_materials} -> {success_item_id} ({base_success_rate:.2%}){ConsoleFrontColor.RESET}",
)
def _index_recipe(
self, materials_key: Tuple[str, str, str], recipe: AlchemyRecipe
) -> None:
for material in recipe.materials:
self._material_index[material].add(materials_key)
self._success_index[recipe.success_item_id].add(materials_key)
self._fail_index[recipe.fail_item_id].add(materials_key)
def _unindex_recipe(
self, materials_key: Tuple[str, str, str], recipe: AlchemyRecipe
) -> None:
for material in recipe.materials:
material_set = self._material_index.get(material)
if material_set and materials_key in material_set:
material_set.discard(materials_key)
if not material_set:
del self._material_index[material]
success_set = self._success_index.get(recipe.success_item_id)
if success_set and materials_key in success_set:
success_set.discard(materials_key)
if not success_set:
del self._success_index[recipe.success_item_id]
fail_set = self._fail_index.get(recipe.fail_item_id)
if fail_set and materials_key in fail_set:
fail_set.discard(materials_key)
if not fail_set:
del self._fail_index[recipe.fail_item_id]
def get_recipes_using_item(self, item_id: str) -> List[AlchemyRecipe]:
if not item_id:
return []
material_keys = sorted(self._material_index.get(item_id, set()))
return [self._recipes[key] for key in material_keys]
def get_recipes_producing_item(
self, item_id: str
) -> Dict[str, List[AlchemyRecipe]]:
if not item_id:
return {"success": [], "fail": []}
success_keys = sorted(
self._success_index.get(item_id, set()),
key=lambda key: (
-self._recipes[key].base_success_rate,
self._recipes[key].materials,
),
)
fail_keys = sorted(
self._fail_index.get(item_id, set()),
key=lambda key: (
-self._recipes[key].base_success_rate,
self._recipes[key].materials,
),
)
return {
"success": [self._recipes[key] for key in success_keys],
"fail": [self._recipes[key] for key in fail_keys],
}
async def callback(
self, message: str, chat_id: int, user_id: int
) -> Optional[str]:
@@ -379,5 +446,176 @@ class WPSAlchemyGame(WPSAPI):
)
__all__ = ["WPSAlchemyGame"]
class WPSAlchemyRecipeLookup(WPSAPI):
def __init__(self) -> None:
super().__init__()
self._alchemy: Optional[WPSAlchemyGame] = None
self._backpack: Optional[WPSBackpackSystem] = None
def dependencies(self) -> List[type]:
return [WPSAlchemyGame, WPSBackpackSystem]
def is_enable_plugin(self) -> bool:
return True
def wake_up(self) -> None:
self._alchemy = Architecture.Get(WPSAlchemyGame)
self._backpack = Architecture.Get(WPSBackpackSystem)
self.register_plugin("炼金配方")
self.register_plugin("alchemy_recipe")
logger.Log(
"Info",
f"{ConsoleFrontColor.GREEN}WPSAlchemyRecipeLookup 插件已加载{ConsoleFrontColor.RESET}",
)
async def callback(
self, message: str, chat_id: int, user_id: int
) -> Optional[str]:
payload = self.parse_message_after_at(message).strip()
if not payload:
return await self.send_markdown_message(self._help_text(), chat_id, user_id)
backpack = self._backpack or Architecture.Get(WPSBackpackSystem)
definition = self._resolve_definition(payload, backpack)
if definition is None:
return await self.send_markdown_message(
f"❌ 未找到物品 `{payload}`,请确认输入的物品 ID 或名称。",
chat_id,
user_id,
)
alchemy = self._alchemy or Architecture.Get(WPSAlchemyGame)
material_recipes = alchemy.get_recipes_using_item(definition.item_id)
produce_map = alchemy.get_recipes_producing_item(definition.item_id)
success_recipes = produce_map["success"]
fail_recipes = produce_map["fail"]
message_text = self._format_markdown(
definition,
material_recipes,
success_recipes,
fail_recipes,
backpack,
)
return await self.send_markdown_message(message_text, chat_id, user_id)
def _resolve_definition(
self, identifier: str, backpack: WPSBackpackSystem
) -> Optional[BackpackItemDefinition]:
lowered = identifier.strip().lower()
cursor = get_db().conn.cursor()
cursor.execute(
f"""
SELECT item_id
FROM {WPSBackpackSystem.ITEMS_TABLE}
WHERE lower(item_id) = ? OR lower(name) = ?
LIMIT 1
""",
(lowered, lowered),
)
row = cursor.fetchone()
item_id = row["item_id"] if row else identifier.strip()
try:
return backpack._get_definition(item_id) # noqa: SLF001
except Exception:
return None
def _format_markdown(
self,
target: BackpackItemDefinition,
material_recipes: List[AlchemyRecipe],
success_recipes: List[AlchemyRecipe],
fail_recipes: List[AlchemyRecipe],
backpack: WPSBackpackSystem,
) -> str:
lines = [
f"# 🔍 炼金配方查询|{target.name}",
f"- 物品 ID`{target.item_id}`",
"---",
]
lines.append("## 作为配方材料")
lines.extend(
self._format_recipe_entries(material_recipes, backpack)
or ["- 暂无记录"]
)
lines.append("\n## 作为成功产物")
lines.extend(
self._format_recipe_entries(success_recipes, backpack, role="success")
or ["- 暂无记录"]
)
lines.append("\n## 作为失败产物")
lines.extend(
self._format_recipe_entries(fail_recipes, backpack, role="fail")
or ["- 暂无记录"]
)
return "\n".join(lines)
def _format_recipe_entries(
self,
recipes: List[AlchemyRecipe],
backpack: WPSBackpackSystem,
*,
role: str = "material",
) -> List[str]:
if not recipes:
return []
entries: List[str] = []
for recipe in recipes:
materials = self._summarize_materials(recipe, backpack)
success_name = self._resolve_item_name(recipe.success_item_id, backpack)
fail_name = self._resolve_item_name(recipe.fail_item_id, backpack)
rate = f"{recipe.base_success_rate:.0%}"
if role == "material":
entry = (
f"- 材料:{materials}|成功产物:`{success_name}`"
f"失败产物:`{fail_name}`|成功率:{rate}"
)
elif role == "success":
entry = (
f"- 材料:{materials}|成功率:{rate}"
f"失败产物:`{fail_name}`"
)
else:
entry = (
f"- 材料:{materials}|成功率:{rate}"
f"成功产物:`{success_name}`"
)
entries.append(entry)
return entries
def _summarize_materials(
self, recipe: AlchemyRecipe, backpack: WPSBackpackSystem
) -> str:
counter = Counter(recipe.materials)
parts: List[str] = []
for item_id, count in sorted(counter.items()):
name = self._resolve_item_name(item_id, backpack)
if count == 1:
parts.append(f"`{name}`")
else:
parts.append(f"`{name}` × {count}")
return " + ".join(parts)
def _resolve_item_name(
self, item_id: str, backpack: WPSBackpackSystem
) -> str:
try:
definition = backpack._get_definition(item_id) # noqa: SLF001
return definition.name
except Exception:
return item_id
def _help_text(self) -> str:
return (
"# ⚗️ 炼金配方查询帮助\n"
"- `炼金配方 <物品ID>`\n"
"- `炼金配方 <物品名称>`\n"
"> 输入需要精确匹配注册物品,名称不区分大小写。"
)
__all__ = ["WPSAlchemyGame", "WPSAlchemyRecipeLookup"]