diff --git a/.cursor/rules/core.mdc b/.cursor/rules/core.mdc index dfee703..d7f7230 100644 --- a/.cursor/rules/core.mdc +++ b/.cursor/rules/core.mdc @@ -14,7 +14,7 @@ alwaysApply: true 你必须完全理解这个项目, 并明白文件夹PWF里的文件你没有权力更改 -你必须熟读 WPSAPI.py 和 plugin_interface.py 才有可能不犯错误 +你必须熟读 @WPSAPI.py 和 @plugin_interface.py 才有可能不犯错误 ### 元指令:模式声明要求 diff --git a/.tasks/2025-11-10_3_alchemy-recipes.md b/.tasks/2025-11-10_3_alchemy-recipes.md new file mode 100644 index 0000000..f0ebf6b --- /dev/null +++ b/.tasks/2025-11-10_3_alchemy-recipes.md @@ -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 +- 更改: 新增炼金配方查询索引接口与查询插件草案,实现“炼金配方”指令基础逻辑 +- 原因: 支持查询炼金配方需求,避免直接访问私有数据结构 +- 阻碍因素: 暂无 +- 状态: 未确认 + +# 最终审查 +待补充 + diff --git a/Plugins/WPSAlchemyGame.py b/Plugins/WPSAlchemyGame.py index 0b0c5a0..04c1c7c 100644 --- a/Plugins/WPSAlchemyGame.py +++ b/Plugins/WPSAlchemyGame.py @@ -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"]