diff --git a/.tasks/2025-11-08_1_store-system.md b/.tasks/2025-11-08_1_store-system.md index 676e0b5..541c273 100644 --- a/.tasks/2025-11-08_1_store-system.md +++ b/.tasks/2025-11-08_1_store-system.md @@ -139,6 +139,12 @@ WPS Bot 插件体系,现有 `WPSConfigAPI` 提供积分管理与签到积分 - 原因:提升商店玩家出售区的可读性与一致性 - 阻碍因素:无 - 状态:未确认 +2025-11-10_00:06:48 +- 已修改:`Plugins/WPSStoreSystem.py` +- 更改:修正 `WPSConfigAPI.get_user_points` 参数传递,仅使用 `user_id`,避免购买流程报错。 +- 原因:购买时出现 `get_user_points()` 参数数量不匹配异常。 +- 阻碍因素:无 +- 状态:未确认 # 最终审查 (待补充) diff --git a/.tasks/2025-11-09_1_garden-system.md b/.tasks/2025-11-09_1_garden-system.md new file mode 100644 index 0000000..4f6622a --- /dev/null +++ b/.tasks/2025-11-09_1_garden-system.md @@ -0,0 +1,86 @@ +# 背景 +文件名: 2025-11-09_1_garden-system.md +创建于: 2025-11-09_22:21:42 +创建者: liubai095\asus +主分支: main +任务分支: (未创建) +Yolo模式: Off + +# 任务描述 +现在我想要新增菜园系统, 仿照qq农场 + +# 项目概览 +菜园系统基于PWF插件体系, 依赖现有WPSConfigSystem、WPSBackpackSystem、WPSStoreSystem、WPSFortuneSystem以及WPSAlchemyGame, 默认每位用户拥有4个可种植方块。 + +# 分析 +- 菜园系统需基于 WPS 插件结构拆分多个入口插件:`菜园`(含子指令售出)、`种植 <种子>`、`收获 <格子序号>`、`偷取 <用户>`。 +- 依赖组件:`WPSConfigAPI`(积分与用户信息)、`WPSBackpackSystem`(注册与存取种子/果实/木材)、`WPSStoreSystem`(整点随机上架种子)、`WPSFortuneSystem`(获取运势值乘以 3% 修正)、`WPSAlchemyGame`(三果炼种/失败得腐败果实)、`ClockScheduler`(成熟定时通知)。 +- 配置项:用户土地块数、收益倍率 `x`(默认 10)、作物品类及成长参数均需从 `ProjectConfig` 读取,可写入默认值。 +- 物品体系:新增 4 种普通草本(积分收益)与 3 种稀有木本(木材收益),名称需统一对应“XX的种子/果实/木材”,额外积分≤种子价×x,额外木材≤10。 +- 商店:种子仅作为整点刷新的系统商品;成熟果实出售时单价=种子价×x,通过商店或菜园子指令对接。 +- 偷取:同一成熟方块仅可被任意用户各偷一次,剩余果实≤一半时不可再偷。 + +# 提议的解决方案 +(待补充) + +# 当前执行步骤:"4. 实施指令插件与集成" + +# 任务进度 +2025-11-09_23:06:20 +- 已修改:Plugins/WPSGardenSystem/* +- 更改:创建菜园数据模型、服务逻辑、基础插件架构以及主要指令插件;注册物品、商店模式与炼金配方,接入调度与运势配置。 +- 原因:实现菜园系统核心功能与用户交互入口。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_00:24:00 +- 已修改:Plugins/WPSGardenSystem/garden_models.py +- 更改:将全部作物的成长时间缩短为原设置的六分之一,以适配更快节奏。 +- 原因:游戏周期需求调整。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_00:30:00 +- 已修改:Plugins/WPSGardenSystem/garden_plugin_base.py +- 更改:优化菜园概览展示,生长中的作物不再显示剩余果实/被偷次数,并将时间改为“YYYY年MM月DD日 HH时MM分SS秒”格式。 +- 原因:提高信息可读性并符合生长状态逻辑。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_00:36:00 +- 已修改:Plugins/WPSGardenSystem/garden_service.py Plugins/WPSGardenSystem/garden_plugin_remove.py Plugins/WPSGardenSystem/__init__.py Plugins/WPSGardenSystem/garden_plugin_base.py +- 更改:新增 `铲除` 指令以立即清空指定地块,服务层提供对应清理接口并更新帮助文案。 +- 原因:支持手动放弃正在生长或已完成的作物。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_00:51:39 +- 已修改:Plugins/WPSGardenSystem/garden_service.py Plugins/WPSGardenSystem/garden_plugin_base.py Plugins/WPSGardenSystem/garden_plugin_plant.py +- 更改:统一使用本地时间存储与展示菜园时间,并提供统一格式化函数,修复预计成熟时间显示不正确问题。 +- 原因:与 ProjectConfig 日志一致,确保用户看到准确的本地时间。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_00:56:34 +- 已修改:Plugins/WPSGardenSystem/garden_service.py +- 更改:当 debug 标志开启时,种植后立即设置作物成熟并跳过计时任务,便于调试。 +- 原因:加速开发环境验证流程。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_01:03:33 +- 已修改:Plugins/WPSGardenSystem/garden_service.py +- 更改:调整 debug 模式逻辑为注册零延迟的调度任务,保持成熟提醒链路,验证调度系统。 +- 原因:在调试时仍需测试调度器推送流程。 +- 阻碍因素:无 +- 状态:未确认 + +2025-11-10_01:08:37 +- 已修改:Plugins/WPSGardenSystem/garden_service.py +- 更改:时间展示改为到分钟级,避免显示秒数以贴合调度频率。 +- 原因:输出信息更简洁,与调度粒度一致。 +- 阻碍因素:无 +- 状态:未确认 + +# 最终审查 +(待补充) diff --git a/Assets/config.json b/Assets/config.json index 1c631fb..0f5c108 100644 --- a/Assets/config.json +++ b/Assets/config.json @@ -15,6 +15,11 @@ "plugin_dir": "Plugins", "alchemy_fortune_coeff": 0.03, "store_hourly_count": 5, + "garden_max_plots_per_user": 4, + "garden_sale_multiplier": 10, + "garden_fortune_coeff": 0.03, + "garden_theft_threshold_ratio": 0.5, + "garden_seed_store_limit": 5, "host": "0.0.0.0", "port": 8000, "verbose": false, diff --git a/Plugins/WPSAlchemyGame.py b/Plugins/WPSAlchemyGame.py index 87bbc96..3110294 100644 --- a/Plugins/WPSAlchemyGame.py +++ b/Plugins/WPSAlchemyGame.py @@ -306,7 +306,7 @@ class WPSAlchemyGame(WPSAPI): details = [] for item_id, count in rewards.items(): try: - definition = backpack._get_definition(item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(item_id) item_name = definition.name except Exception: item_name = item_id @@ -357,7 +357,7 @@ class WPSAlchemyGame(WPSAPI): item_id = row["item_id"] if row else identifier.strip() backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) try: - return backpack._get_definition(item_id) # type: ignore[attr-defined] + return backpack._get_definition(item_id) except Exception: return None diff --git a/Plugins/WPSGardenSystem/__init__.py b/Plugins/WPSGardenSystem/__init__.py new file mode 100644 index 0000000..1e0340d --- /dev/null +++ b/Plugins/WPSGardenSystem/__init__.py @@ -0,0 +1,23 @@ +from .garden_models import ( + GARDEN_CROPS, + GardenCropDefinition, + GardenExtraReward, +) +from .garden_service import GardenService +from .garden_plugin_view import WPSGardenView +from .garden_plugin_plant import WPSGardenPlant +from .garden_plugin_harvest import WPSGardenHarvest +from .garden_plugin_steal import WPSGardenSteal +from .garden_plugin_remove import WPSGardenRemove + +__all__ = [ + "GardenCropDefinition", + "GardenExtraReward", + "GARDEN_CROPS", + "GardenService", + "WPSGardenView", + "WPSGardenPlant", + "WPSGardenHarvest", + "WPSGardenSteal", + "WPSGardenRemove", +] diff --git a/Plugins/WPSGardenSystem/garden_models.py b/Plugins/WPSGardenSystem/garden_models.py new file mode 100644 index 0000000..d6117a0 --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_models.py @@ -0,0 +1,179 @@ +"""Garden system crop definitions and configuration models.""" + +from __future__ import annotations + +from typing import Dict, List, Tuple + +from PWF.Convention.Runtime.Architecture import Architecture +from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig +from PWF.CoreModules.plugin_interface import DatabaseModel +from pydantic import BaseModel, Field + +# Shared logger/config +_config: ProjectConfig = Architecture.Get(ProjectConfig) +_config.SaveProperties() + + +class GardenExtraReward(BaseModel): + kind: str = Field(..., description="points 或 item") + payload: Dict[str, int] = Field(default_factory=dict) + base_rate: float = Field(..., ge=0.0, le=1.0) + + class Config: + allow_mutation = False + + +class GardenCropDefinition(BaseModel): + seed_id: str + fruit_id: str + display_name: str + tier: str # common / rare + growth_minutes: int + seed_price: int + base_yield: int + extra_reward: GardenExtraReward + extra_item_id: str | None = None + + class Config: + allow_mutation = False + + +GARDEN_CONFIG_DEFAULTS: Dict[str, int | float] = { + "garden_max_plots_per_user": 4, + "garden_sale_multiplier": 10, + "garden_fortune_coeff": 0.03, + "garden_theft_threshold_ratio": 0.5, + "garden_seed_store_limit": 5, +} + +for key, value in GARDEN_CONFIG_DEFAULTS.items(): + _config.FindItem(key, value) +_config.SaveProperties() + +COMMON_HERB_CROPS: Tuple[GardenCropDefinition, ...] = ( + GardenCropDefinition( + seed_id="garden_seed_mint", + fruit_id="garden_fruit_mint", + display_name="薄荷", + tier="common", + growth_minutes=30, + seed_price=30, + base_yield=4, + extra_reward=GardenExtraReward(kind="points", payload={"min": 10, "max": 120}, base_rate=0.6), + ), + GardenCropDefinition( + seed_id="garden_seed_basil", + fruit_id="garden_fruit_basil", + display_name="罗勒", + tier="common", + growth_minutes=40, + seed_price=36, + base_yield=5, + extra_reward=GardenExtraReward(kind="points", payload={"min": 15, "max": 150}, base_rate=0.55), + ), + GardenCropDefinition( + seed_id="garden_seed_sage", + fruit_id="garden_fruit_sage", + display_name="鼠尾草", + tier="common", + growth_minutes=50, + seed_price=42, + base_yield=5, + extra_reward=GardenExtraReward(kind="points", payload={"min": 20, "max": 180}, base_rate=0.5), + ), + GardenCropDefinition( + seed_id="garden_seed_rosemary", + fruit_id="garden_fruit_rosemary", + display_name="迷迭香", + tier="common", + growth_minutes=60, + seed_price=50, + base_yield=6, + extra_reward=GardenExtraReward(kind="points", payload={"min": 30, "max": 220}, base_rate=0.45), + ), +) + +RARE_TREE_CROPS: Tuple[GardenCropDefinition, ...] = ( + GardenCropDefinition( + seed_id="garden_seed_ginkgo", + fruit_id="garden_fruit_ginkgo", + display_name="银杏", + tier="rare", + growth_minutes=120, + seed_price=120, + base_yield=3, + extra_reward=GardenExtraReward(kind="item", payload={"min": 2, "max": 6}, base_rate=0.5), + extra_item_id="garden_wood_ginkgo", + ), + GardenCropDefinition( + seed_id="garden_seed_sakura", + fruit_id="garden_fruit_sakura", + display_name="樱花", + tier="rare", + growth_minutes=160, + seed_price=150, + base_yield=3, + extra_reward=GardenExtraReward(kind="item", payload={"min": 3, "max": 8}, base_rate=0.45), + extra_item_id="garden_wood_sakura", + ), + GardenCropDefinition( + seed_id="garden_seed_maple", + fruit_id="garden_fruit_maple", + display_name="红枫", + tier="rare", + growth_minutes=180, + seed_price=180, + base_yield=4, + extra_reward=GardenExtraReward(kind="item", payload={"min": 4, "max": 10}, base_rate=0.4), + extra_item_id="garden_wood_maple", + ), +) + +GARDEN_CROPS: Dict[str, GardenCropDefinition] = { + crop.seed_id: crop for crop in (*COMMON_HERB_CROPS, *RARE_TREE_CROPS) +} +GARDEN_FRUITS: Dict[str, GardenCropDefinition] = { + crop.fruit_id: crop for crop in (*COMMON_HERB_CROPS, *RARE_TREE_CROPS) +} + +GARDEN_MISC_ITEMS = { + "garden_item_rot_fruit": { + "name": "腐败的果实", + "tier": "common", + } +} + + +def get_garden_db_models() -> List[DatabaseModel]: + return [ + DatabaseModel( + table_name="garden_plots", + column_defs={ + "user_id": "INTEGER NOT NULL", + "chat_id": "INTEGER NOT NULL", + "plot_index": "INTEGER NOT NULL", + "seed_id": "TEXT NOT NULL", + "seed_quality": "TEXT NOT NULL DEFAULT 'common'", + "planted_at": "TEXT NOT NULL", + "mature_at": "TEXT NOT NULL", + "is_mature": "INTEGER NOT NULL DEFAULT 0", + "base_yield": "INTEGER NOT NULL", + "extra_type": "TEXT", + "extra_payload": "TEXT", + "remaining_fruit": "INTEGER NOT NULL", + "theft_users": "TEXT DEFAULT '[]'", + "scheduled_task_id": "INTEGER", + "PRIMARY KEY (user_id, plot_index)": "", + }, + ), + ] + + +__all__ = [ + "GardenCropDefinition", + "GardenExtraReward", + "GARDEN_CROPS", + "GARDEN_FRUITS", + "GARDEN_MISC_ITEMS", + "get_garden_db_models", +] diff --git a/Plugins/WPSGardenSystem/garden_plugin_base.py b/Plugins/WPSGardenSystem/garden_plugin_base.py new file mode 100644 index 0000000..0a9587a --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_base.py @@ -0,0 +1,222 @@ +"""Shared base class for garden plugins.""" + +from __future__ import annotations + +import json +from typing import List, Optional, Type + +from PWF.Convention.Runtime.Architecture import Architecture +from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig +from PWF.CoreModules.plugin_interface import DatabaseModel + +from Plugins.WPSAPI import WPSAPI +from Plugins.WPSBackpackSystem import ( + BackpackItemTier, + WPSBackpackSystem, +) +from Plugins.WPSStoreSystem import WPSStoreSystem +from Plugins.WPSConfigSystem import WPSConfigAPI +from Plugins.WPSFortuneSystem import WPSFortuneSystem +from Plugins.WPSAlchemyGame import WPSAlchemyGame + +from .garden_models import ( + GARDEN_CROPS, + GARDEN_FRUITS, + GARDEN_MISC_ITEMS, + GardenCropDefinition, + get_garden_db_models, +) +from .garden_service import GardenService + + +class WPSGardenBase(WPSAPI): + _service: GardenService | None = None + _initialized: bool = False + + @classmethod + def service(cls) -> GardenService: + if cls._service is None: + cls._service = GardenService() + cls._service.recover_overdue_plots() + return cls._service + + def dependencies(self) -> List[Type]: + return [ + WPSConfigAPI, + WPSBackpackSystem, + WPSStoreSystem, + WPSFortuneSystem, + WPSAlchemyGame, + ] + + def register_db_model(self) -> List[DatabaseModel]: + return get_garden_db_models() + + def wake_up(self) -> None: + if WPSGardenBase._initialized: + return + WPSGardenBase._initialized = True + logger: ProjectConfig = Architecture.Get(ProjectConfig) + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + store: WPSStoreSystem = Architecture.Get(WPSStoreSystem) + alchemy: WPSAlchemyGame = Architecture.Get(WPSAlchemyGame) + service = self.service() + + for crop in GARDEN_CROPS.values(): + seed_name = f"{crop.display_name}的种子" + fruit_name = f"{crop.display_name}的果实" + tier = BackpackItemTier.COMMON if crop.tier == "common" else BackpackItemTier.RARE + self._safe_register_item(backpack, crop.seed_id, seed_name, tier) + self._safe_register_item(backpack, crop.fruit_id, fruit_name, tier) + if crop.extra_reward and crop.extra_reward.kind == "item" and crop.extra_item_id: + wood_name = f"{crop.display_name}的木材" + self._safe_register_item(backpack, crop.extra_item_id, wood_name, BackpackItemTier.RARE) + + self._safe_register_mode( + store, + crop, + limit_amount=service.config.seed_store_limit, + ) + self._safe_register_recipe(alchemy, crop) + + for item_id, meta in GARDEN_MISC_ITEMS.items(): + self._safe_register_item( + backpack, + item_id, + meta["name"], + BackpackItemTier.COMMON, + ) + + logger.Log( + "Info", + f"{ConsoleFrontColor.GREEN}WPSGarden 系统完成物品与商店初始化{ConsoleFrontColor.RESET}", + ) + + # region Helpers + def _safe_register_item( + self, + backpack: WPSBackpackSystem, + item_id: str, + name: str, + tier: BackpackItemTier, + ) -> None: + try: + backpack.register_item(item_id, name, tier) + except Exception: + pass + + def _safe_register_mode( + self, + store: WPSStoreSystem, + crop: GardenCropDefinition, + *, + limit_amount: int, + ) -> None: + try: + store.register_mode( + item_id=crop.seed_id, + price=crop.seed_price, + limit_amount=limit_amount, + ) + except Exception: + pass + + def _safe_register_recipe( + self, + alchemy: WPSAlchemyGame, + crop: GardenCropDefinition, + ) -> None: + try: + success_rate = 0.75 if crop.tier == "common" else 0.6 + alchemy.register_recipe( + (crop.fruit_id, crop.fruit_id, crop.fruit_id), + crop.seed_id, + "garden_item_rot_fruit", + success_rate, + ) + except Exception: + pass + + async def _clock_mark_mature(self, user_id: int, chat_id: int, plot_index: int) -> None: + service = self.service() + plot = service.get_plot(user_id, plot_index) + if not plot: + return + if int(plot["is_mature"]) == 1: + return + service.mark_mature(user_id, plot_index) + crop = GARDEN_CROPS.get(plot["seed_id"]) + if crop is None: + return + message = ( + "# 🌾 作物成熟提醒\n" + f"- 地块 {plot_index}:{crop.display_name} 已成熟,记得收获!" + ) + await self.send_markdown_message(message, chat_id, user_id) + + def _format_timestamp(self, ts: str) -> str: + return self.service().format_display_time(ts) + + def resolve_seed_id(self, keyword: str) -> Optional[GardenCropDefinition]: + key = keyword.strip().lower() + for crop in GARDEN_CROPS.values(): + if crop.seed_id.lower() == key: + return crop + if crop.display_name.lower() == key: + return crop + if f"{crop.display_name}的种子".lower() == key: + return crop + return None + + def resolve_fruit_id(self, keyword: str) -> Optional[GardenCropDefinition]: + key = keyword.strip().lower() + for crop in GARDEN_FRUITS.values(): + if crop.fruit_id.lower() == key: + return crop + if crop.display_name.lower() == key: + return crop + if f"{crop.display_name}的果实".lower() == key: + return crop + return None + + def format_garden_overview(self, user_id: int) -> str: + service = self.service() + plots = service.list_plots(user_id) + config = service.config + lines = ["# 🌱 菜园概览"] + if not plots: + lines.append("> 尚未种植任何作物,使用 `种植 <种子>` 开始耕种。") + else: + for plot in plots: + crop = GARDEN_CROPS.get(plot["seed_id"], None) + name = crop.display_name if crop else plot["seed_id"] + idx = plot["plot_index"] + is_mature = bool(plot["is_mature"]) + mature_at = plot["mature_at"] + formatted_time = self._format_timestamp(mature_at) + if is_mature: + remaining = plot["remaining_fruit"] + theft_users = len(json.loads(plot["theft_users"])) if plot.get("theft_users") else 0 + status = f"✅ 已成熟(成熟于 {formatted_time})" + lines.append( + f"- 地块 {idx}|{name}|{status}|剩余果实 {remaining}|被偷次数 {theft_users}" + ) + else: + status = f"⌛ 生长中,预计成熟 {formatted_time}" + lines.append(f"- 地块 {idx}|{name}|{status}") + available = config.max_plots - len(plots) + if available > 0: + lines.append(f"\n> 尚有 {available} 块空地可用。") + lines.append( + "\n---\n- `种植 <种子>`:消耗种子种下作物\n" + "- `收获 <地块序号>`:收成成熟作物\n" + "- `偷取 <用户> <地块序号>`:从他人成熟作物中偷取果实\n" + "- `铲除 <地块序号>`:立即清空指定地块\n" + "- `菜园 售出 <果实> <数量>`:出售果实换取积分" + ) + return "\n".join(lines) + + # endregion + + +__all__ = ["WPSGardenBase"] diff --git a/Plugins/WPSGardenSystem/garden_plugin_harvest.py b/Plugins/WPSGardenSystem/garden_plugin_harvest.py new file mode 100644 index 0000000..b3a6bca --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_harvest.py @@ -0,0 +1,78 @@ +"""Harvest plugin for garden system.""" + +from __future__ import annotations + +from typing import Optional + +from PWF.Convention.Runtime.Architecture import Architecture + +from Plugins.WPSBackpackSystem import WPSBackpackSystem +from Plugins.WPSConfigSystem import WPSConfigAPI +from Plugins.WPSFortuneSystem import WPSFortuneSystem + +from .garden_plugin_base import WPSGardenBase + + +class WPSGardenHarvest(WPSGardenBase): + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("harvest") + self.register_plugin("收获") + + 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("❌ 指令格式:`收获 <地块序号>`", chat_id, user_id) + tokens = [token.strip() for token in payload.split() if token.strip()] + if not tokens or not tokens[0].isdigit(): + return await self.send_markdown_message("❌ 指令格式:`收获 <地块序号>`", chat_id, user_id) + plot_index = int(tokens[0]) + + fortune: WPSFortuneSystem = Architecture.Get(WPSFortuneSystem) + fortune_value = fortune.get_fortune_value(user_id) + try: + result = self.service().harvest( + user_id=user_id, + plot_index=plot_index, + fortune_value=fortune_value, + ) + except ValueError as exc: + return await self.send_markdown_message(f"❌ {exc}", chat_id, user_id) + + crop = result["crop"] + base_qty = result["base_yield"] + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + backpack.add_item(user_id, crop.fruit_id, base_qty) + + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) + extra_lines = [] + if result["extra"]: + extra = result["extra"] + if extra["type"] == "points": + gained = int(extra["amount"]) + if gained > 0: + new_points = await config_api.adjust_user_points( + chat_id, + user_id, + gained, + reason=f"收获 {crop.display_name} 的额外积分", + ) + extra_lines.append(f"- 额外积分:+{gained}(当前积分 {new_points})") + elif extra["type"] == "item": + item_id = extra["item_id"] + qty = int(extra["quantity"]) + if qty > 0: + backpack.add_item(user_id, item_id, qty) + extra_lines.append(f"- 额外物品:{item_id} × {qty}") + + message_lines = [ + "# ✅ 收获成功", + f"- 地块:{plot_index}", + f"- 作物:{crop.display_name}", + f"- 基础果实:{crop.display_name}的果实 × {base_qty}", + ] + message_lines.extend(extra_lines) + return await self.send_markdown_message("\n".join(message_lines), chat_id, user_id) + + +__all__ = ["WPSGardenHarvest"] diff --git a/Plugins/WPSGardenSystem/garden_plugin_plant.py b/Plugins/WPSGardenSystem/garden_plugin_plant.py new file mode 100644 index 0000000..50857f0 --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_plant.py @@ -0,0 +1,72 @@ +"""Planting plugin for garden system.""" + +from __future__ import annotations + +from typing import Optional + +from PWF.Convention.Runtime.Architecture import Architecture + +from Plugins.WPSBackpackSystem import WPSBackpackSystem + +from .garden_plugin_base import WPSGardenBase + + +class WPSGardenPlant(WPSGardenBase): + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("plant") + self.register_plugin("种植") + + 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("❌ 指令格式:`种植 <种子> [地块序号]`", chat_id, user_id) + + tokens = [token.strip() for token in payload.split() if token.strip()] + if not tokens: + return await self.send_markdown_message("❌ 指令格式:`种植 <种子> [地块序号]`", chat_id, user_id) + + plot_index: Optional[int] = None + if len(tokens) >= 2 and tokens[-1].isdigit(): + plot_index = int(tokens[-1]) + identifier = " ".join(tokens[:-1]) + else: + identifier = " ".join(tokens) + + crop = self.resolve_seed_id(identifier) + if crop is None: + return await self.send_markdown_message("❌ 未找到对应种子", chat_id, user_id) + + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + owned = 0 + for item in backpack.get_user_items(user_id): + if item.item_id == crop.seed_id: + owned = item.quantity + break + if owned <= 0: + return await self.send_markdown_message("❌ 背包中没有该种子", chat_id, user_id) + + try: + assigned_plot, mature_at_iso = self.service().plant( + user_id=user_id, + chat_id=chat_id, + seed_id=crop.seed_id, + plot_index=plot_index, + register_callback=(self, "_clock_mark_mature"), + ) + except ValueError as exc: + return await self.send_markdown_message(f"❌ {exc}", chat_id, user_id) + + backpack.set_item_quantity(user_id, crop.seed_id, owned - 1) + + maturity_label = self.service().format_display_time(mature_at_iso) + message_body = ( + "# 🌱 种植成功\n" + f"- 地块:{assigned_plot}\n" + f"- 作物:{crop.display_name}\n" + f"- 预计成熟:{maturity_label}" + ) + return await self.send_markdown_message(message_body, chat_id, user_id) + + +__all__ = ["WPSGardenPlant"] diff --git a/Plugins/WPSGardenSystem/garden_plugin_remove.py b/Plugins/WPSGardenSystem/garden_plugin_remove.py new file mode 100644 index 0000000..66e62a2 --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_remove.py @@ -0,0 +1,39 @@ +"""Remove (clear) plot plugin for garden system.""" + +from __future__ import annotations + +from typing import Optional + +from .garden_plugin_base import WPSGardenBase + + +class WPSGardenRemove(WPSGardenBase): + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("remove") + self.register_plugin("铲除") + + 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("❌ 指令格式:`铲除 <地块序号>`", chat_id, user_id) + tokens = [token.strip() for token in payload.split() if token.strip()] + if not tokens or not tokens[0].isdigit(): + return await self.send_markdown_message("❌ 指令格式:`铲除 <地块序号>`", chat_id, user_id) + plot_index = int(tokens[0]) + if plot_index <= 0: + return await self.send_markdown_message("❌ 地块序号必须为正整数", chat_id, user_id) + + success = self.service().clear_plot(user_id=user_id, plot_index=plot_index) + if not success: + return await self.send_markdown_message("❌ 指定地块不存在或已为空", chat_id, user_id) + + message_body = ( + "# 🧹 铲除完成\n" + f"- 已清空地块 {plot_index}\n" + "- 可以重新种植新的作物" + ) + return await self.send_markdown_message(message_body, chat_id, user_id) + + +__all__ = ["WPSGardenRemove"] diff --git a/Plugins/WPSGardenSystem/garden_plugin_steal.py b/Plugins/WPSGardenSystem/garden_plugin_steal.py new file mode 100644 index 0000000..3b0d32b --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_steal.py @@ -0,0 +1,86 @@ +"""Steal plugin for garden system.""" + +from __future__ import annotations + +from typing import Optional + +from PWF.Convention.Runtime.Architecture import Architecture + +from PWF.CoreModules.database import get_db +from Plugins.WPSBackpackSystem import WPSBackpackSystem + +from .garden_plugin_base import WPSGardenBase + + +class WPSGardenSteal(WPSGardenBase): + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("steal") + self.register_plugin("偷取") + + 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("❌ 指令格式:`偷取 <用户> <地块序号>`", chat_id, user_id) + tokens = [token.strip() for token in payload.split() if token.strip()] + if len(tokens) < 2: + return await self.send_markdown_message("❌ 指令格式:`偷取 <用户> <地块序号>`", chat_id, user_id) + target_identifier = tokens[0] + if not tokens[1].isdigit(): + return await self.send_markdown_message("❌ 指令格式:`偷取 <用户> <地块序号>`", chat_id, user_id) + plot_index = int(tokens[1]) + + owner_id = self._resolve_user_identifier(target_identifier) + if owner_id is None: + return await self.send_markdown_message("❌ 未找到目标用户", chat_id, user_id) + if owner_id == user_id: + return await self.send_markdown_message("❌ 不能偷取自己的菜园", chat_id, user_id) + + try: + result = self.service().steal( + thief_id=user_id, + owner_id=owner_id, + plot_index=plot_index, + ) + except ValueError as exc: + return await self.send_markdown_message(f"❌ {exc}", chat_id, user_id) + + crop = result["crop"] + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + backpack.add_item(user_id, crop.fruit_id, result["stolen_quantity"]) + + remaining = result["remaining"] + message = ( + "# 🕵️ 偷取成功\n" + f"- 目标:{crop.display_name}\n" + f"- 获得:{crop.display_name}的果实 × {result['stolen_quantity']}\n" + f"- 目标剩余果实:{remaining}" + ) + await self.send_markdown_message(message, chat_id, user_id) + + owner_chat = result.get("chat_id") + if owner_chat: + owner_message = ( + "# ⚠️ 菜园警报\n" + f"- 你的 {crop.display_name} 被偷走了 1 个果实\n" + f"- 当前剩余果实:{remaining}" + ) + await self.send_markdown_message(owner_message, owner_chat, owner_id) + return None + + def _resolve_user_identifier(self, identifier: str) -> Optional[int]: + text = identifier.strip() + if text.isdigit(): + return int(text) + cursor = get_db().conn.cursor() + cursor.execute( + "SELECT user_id FROM user_info WHERE username = ? COLLATE NOCASE", + (text,), + ) + row = cursor.fetchone() + if row: + return int(row["user_id"]) + return None + + +__all__ = ["WPSGardenSteal"] diff --git a/Plugins/WPSGardenSystem/garden_plugin_view.py b/Plugins/WPSGardenSystem/garden_plugin_view.py new file mode 100644 index 0000000..bbc3e2b --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_view.py @@ -0,0 +1,89 @@ +"""Garden overview and selling plugin.""" + +from __future__ import annotations + +from typing import Optional + +from PWF.Convention.Runtime.Architecture import Architecture + +from Plugins.WPSBackpackSystem import WPSBackpackSystem +from Plugins.WPSConfigSystem import WPSConfigAPI + +from .garden_plugin_base import WPSGardenBase + + +class WPSGardenView(WPSGardenBase): + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("garden") + self.register_plugin("菜园") + + 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_overview(chat_id, user_id) + + tokens = [token.strip() for token in payload.split() if token.strip()] + if tokens and tokens[0] in {"售出", "sell"}: + return await self._handle_sell(tokens[1:], chat_id, user_id) + + return await self._send_overview(chat_id, user_id) + + async def _send_overview(self, chat_id: int, user_id: int) -> Optional[str]: + overview = self.format_garden_overview(user_id) + return await self.send_markdown_message(overview, chat_id, user_id) + + async def _handle_sell(self, args: list[str], chat_id: int, user_id: int) -> Optional[str]: + if len(args) < 2: + return await self.send_markdown_message( + "❌ 指令格式:`菜园 售出 <果实> <数量>`", + chat_id, + user_id, + ) + identifier = args[0] + try: + quantity = int(args[1]) + except ValueError: + return await self.send_markdown_message("❌ 数量必须是整数", chat_id, user_id) + if quantity <= 0: + return await self.send_markdown_message("❌ 数量必须大于0", chat_id, user_id) + + crop = self.resolve_fruit_id(identifier) + if crop is None: + return await self.send_markdown_message("❌ 未找到对应果实", chat_id, user_id) + + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + owned = 0 + for item in backpack.get_user_items(user_id): + if item.item_id == crop.fruit_id: + owned = item.quantity + break + if owned < quantity: + return await self.send_markdown_message("❌ 果实数量不足", chat_id, user_id) + + total_points, price_per = self.service().sell_fruit( + user_id=user_id, + fruit_id=crop.fruit_id, + quantity=quantity, + ) + backpack.set_item_quantity(user_id, crop.fruit_id, owned - quantity) + + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) + new_points = await config_api.adjust_user_points( + chat_id, + user_id, + total_points, + reason=f"出售 {crop.display_name} 的果实", + ) + + message = ( + "# 🛒 售出成功\n" + f"- 果实:{crop.display_name} × {quantity}\n" + f"- 单价:{price_per} 分\n" + f"- 总计:{total_points} 分\n" + f"- 当前积分:{new_points}" + ) + return await self.send_markdown_message(message, chat_id, user_id) + + +__all__ = ["WPSGardenView"] diff --git a/Plugins/WPSGardenSystem/garden_service.py b/Plugins/WPSGardenSystem/garden_service.py new file mode 100644 index 0000000..0ad76ed --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_service.py @@ -0,0 +1,330 @@ +"""Garden service handling planting, harvesting, stealing, and selling.""" + +from __future__ import annotations + +import json +import random +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple + +from PWF.Convention.Runtime.Architecture import Architecture +from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig +from PWF.CoreModules.database import get_db +from PWF.CoreModules.plugin_interface import PluginInterface +from PWF.CoreModules.flags import get_internal_debug +from pydantic import BaseModel + +from .garden_models import ( + GARDEN_CROPS, + GARDEN_FRUITS, + GARDEN_MISC_ITEMS, + GardenCropDefinition, + get_garden_db_models, +) + +Timestamp = str + + +def _local_now() -> datetime: + return datetime.now() + + +def _parse_local_iso(ts: str) -> datetime: + return datetime.fromisoformat(ts) + + +class GardenConfig(BaseModel): + max_plots: int + sale_multiplier: int + fortune_coeff: float + theft_threshold_ratio: float + seed_store_limit: int + + class Config: + allow_mutation = False + + @classmethod + def load(cls) -> "GardenConfig": + project_config: ProjectConfig = Architecture.Get(ProjectConfig) + max_plots = int(project_config.FindItem("garden_max_plots_per_user", 4)) + sale_multiplier = int(project_config.FindItem("garden_sale_multiplier", 10)) + fortune_coeff = float(project_config.FindItem("garden_fortune_coeff", 0.03)) + theft_ratio = float(project_config.FindItem("garden_theft_threshold_ratio", 0.5)) + seed_store_limit = int(project_config.FindItem("garden_seed_store_limit", 5)) + project_config.SaveProperties() + return cls( + max_plots=max_plots, + sale_multiplier=sale_multiplier, + fortune_coeff=fortune_coeff, + theft_threshold_ratio=theft_ratio, + seed_store_limit=seed_store_limit, + ) + + +class GardenService: + def __init__(self) -> None: + self._config = GardenConfig.load() + self._db = get_db() + self._logger: ProjectConfig = Architecture.Get(ProjectConfig) + + @property + def config(self) -> GardenConfig: + return self._config + + # region Query helpers + def list_plots(self, user_id: int) -> List[Dict[str, object]]: + cursor = self._db.conn.cursor() + cursor.execute( + "SELECT * FROM garden_plots WHERE user_id = ? ORDER BY plot_index ASC", + (user_id,), + ) + rows = cursor.fetchall() + return [dict(row) for row in rows] + + def get_plot(self, user_id: int, plot_index: int) -> Optional[Dict[str, object]]: + cursor = self._db.conn.cursor() + cursor.execute( + "SELECT * FROM garden_plots WHERE user_id = ? AND plot_index = ?", + (user_id, plot_index), + ) + row = cursor.fetchone() + return dict(row) if row else None + + # endregion + + # region Planting + def plant( + self, + *, + user_id: int, + chat_id: int, + seed_id: str, + plot_index: Optional[int] = None, + register_callback: Optional[ + Tuple[PluginInterface, str] + ] = None, + ) -> Tuple[int, datetime]: + crop = GARDEN_CROPS.get(seed_id) + if not crop: + raise ValueError("未知的种子") + plots = self.list_plots(user_id) + used_indices = {int(plot["plot_index"]) for plot in plots} + if len(used_indices) >= self._config.max_plots: + raise ValueError("没有空闲土地") + if plot_index is None: + for idx in range(1, self._config.max_plots + 1): + if idx not in used_indices: + plot_index = idx + break + if plot_index is None: + raise ValueError("无法分配地块") + planted_at = _local_now() + mature_at = planted_at + timedelta(minutes=crop.growth_minutes) + debug_mode = get_internal_debug() + if debug_mode: + mature_at = planted_at + cursor = self._db.conn.cursor() + cursor.execute( + """ + INSERT INTO garden_plots ( + user_id, chat_id, plot_index, seed_id, seed_quality, planted_at, mature_at, + is_mature, base_yield, extra_type, extra_payload, remaining_fruit, theft_users, scheduled_task_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, NULL) + """, + ( + user_id, + chat_id, + plot_index, + crop.seed_id, + crop.tier, + planted_at.isoformat(), + mature_at.isoformat(), + crop.base_yield, + crop.extra_reward.kind, + json.dumps(crop.extra_reward.payload) if crop.extra_reward else None, + crop.base_yield, + json.dumps([]), + ), + ) + self._db.conn.commit() + + task_id = None + if register_callback: + plugin, callback_name = register_callback + delay_ms = 0 if debug_mode else int(crop.growth_minutes * 60 * 1000) + task_id = plugin.register_clock( + getattr(plugin, callback_name), + delay_ms, + kwargs={"user_id": user_id, "chat_id": chat_id, "plot_index": plot_index}, + ) + cursor.execute( + "UPDATE garden_plots SET scheduled_task_id = ? WHERE user_id = ? AND plot_index = ?", + (task_id, user_id, plot_index), + ) + self._db.conn.commit() + return plot_index, mature_at.isoformat() + + def mark_mature(self, user_id: int, plot_index: int) -> None: + cursor = self._db.conn.cursor() + cursor.execute( + "UPDATE garden_plots SET is_mature = 1, scheduled_task_id = NULL WHERE user_id = ? AND plot_index = ?", + (user_id, plot_index), + ) + self._db.conn.commit() + + # endregion + + # region Harvest + def harvest(self, *, user_id: int, plot_index: int, fortune_value: float) -> Dict[str, object]: + plot = self.get_plot(user_id, plot_index) + if not plot: + raise ValueError("指定地块不存在") + if int(plot["is_mature"]) != 1: + raise ValueError("作物尚未成熟") + crop = GARDEN_CROPS.get(plot["seed_id"]) + if not crop: + raise ValueError("未知作物") + base_yield = int(plot["base_yield"]) + extra_reward = None + if crop.extra_reward: + base_rate = crop.extra_reward.base_rate + probability = max( + 0.0, + min(1.0, base_rate + fortune_value * self._config.fortune_coeff), + ) + if random.random() <= probability: + if crop.extra_reward.kind == "points": + data = crop.extra_reward.payload + max_points = min( + data.get("max", base_yield * crop.seed_price), + crop.seed_price * self._config.sale_multiplier, + ) + min_points = data.get("min", 0) + if max_points > 0: + amount = random.randint(min_points, max_points) + extra_reward = {"type": "points", "amount": amount} + elif crop.extra_reward.kind == "item" and crop.extra_item_id: + data = crop.extra_reward.payload + min_qty = max(0, data.get("min", 0)) + max_qty = max(min_qty, data.get("max", min_qty)) + if max_qty > 0: + quantity = random.randint(min_qty, max_qty) + extra_reward = { + "type": "item", + "item_id": crop.extra_item_id, + "quantity": quantity, + } + result = { + "crop": crop, + "base_yield": base_yield, + "extra": extra_reward, + } + cursor = self._db.conn.cursor() + cursor.execute( + "DELETE FROM garden_plots WHERE user_id = ? AND plot_index = ?", + (user_id, plot_index), + ) + self._db.conn.commit() + return result + + def clear_plot(self, *, user_id: int, plot_index: int) -> bool: + cursor = self._db.conn.cursor() + cursor.execute( + "DELETE FROM garden_plots WHERE user_id = ? AND plot_index = ?", + (user_id, plot_index), + ) + deleted = cursor.rowcount > 0 + if deleted: + self._db.conn.commit() + return deleted + + # endregion + + # region Steal + def steal(self, *, thief_id: int, owner_id: int, plot_index: int) -> Dict[str, object]: + plot = self.get_plot(owner_id, plot_index) + if not plot: + raise ValueError("目标地块不存在") + if int(plot["is_mature"]) != 1: + raise ValueError("目标作物尚未成熟") + crop = GARDEN_CROPS.get(plot["seed_id"]) + if not crop: + raise ValueError("未知作物") + remaining = int(plot["remaining_fruit"]) + threshold = int(round(int(plot["base_yield"]) * self._config.theft_threshold_ratio)) + if remaining <= threshold: + raise ValueError("果实剩余不足,无法偷取") + theft_users = set(json.loads(plot["theft_users"])) + if thief_id in theft_users: + raise ValueError("你已经偷取过该作物") + theft_users.add(thief_id) + remaining -= 1 + cursor = self._db.conn.cursor() + cursor.execute( + """ + UPDATE garden_plots SET remaining_fruit = ?, theft_users = ? + WHERE user_id = ? AND plot_index = ? + """, + ( + remaining, + json.dumps(list(theft_users)), + owner_id, + plot_index, + ), + ) + self._db.conn.commit() + return { + "crop": crop, + "stolen_quantity": 1, + "remaining": remaining, + "chat_id": plot["chat_id"], + } + + # endregion + + # region Selling + def sell_fruit(self, *, user_id: int, fruit_id: str, quantity: int) -> Tuple[int, int]: + crop = GARDEN_FRUITS.get(fruit_id) + if not crop: + raise ValueError("未知果实") + if quantity <= 0: + raise ValueError("数量必须大于0") + price_per = crop.seed_price * self._config.sale_multiplier + total_points = price_per * quantity + return total_points, price_per + + # endregion + + # region Maintenance + def recover_overdue_plots(self) -> None: + cursor = self._db.conn.cursor() + cursor.execute( + "SELECT user_id, plot_index, mature_at, is_mature FROM garden_plots WHERE is_mature = 0", + ) + rows = cursor.fetchall() + now = _local_now() + updated = 0 + for row in rows: + mature_at = _parse_local_iso(row["mature_at"]) + if mature_at <= now: + self.mark_mature(row["user_id"], row["plot_index"]) + updated += 1 + if updated: + self._logger.Log( + "Info", + f"{ConsoleFrontColor.GREEN}同步成熟地块 {updated} 个{ConsoleFrontColor.RESET}", + ) + + # endregion + + # region Utilities + def format_display_time(self, iso_ts: str) -> str: + try: + dt = _parse_local_iso(iso_ts) + return dt.strftime("%Y年%m月%d日 %H时%M分") + except Exception: + return iso_ts + + # endregion + +__all__ = ["GardenService", "GardenConfig", "get_garden_db_models"] diff --git a/Plugins/WPSStoreSystem.py b/Plugins/WPSStoreSystem.py index 21f2dec..df894f6 100644 --- a/Plugins/WPSStoreSystem.py +++ b/Plugins/WPSStoreSystem.py @@ -167,7 +167,7 @@ class WPSStoreSystem(WPSAPI): backpack = Architecture.Get(WPSBackpackSystem) try: - item_def = backpack._get_definition(item_id) # type: ignore[attr-defined] + item_def = backpack._get_definition(item_id) except Exception as exc: raise ValueError(f"Item {item_id} not registered in backpack system") from exc @@ -292,7 +292,7 @@ class WPSStoreSystem(WPSAPI): now = datetime.now(timezone.utc).isoformat() backpack = Architecture.Get(WPSBackpackSystem) for mode in selection: - definition = backpack._get_definition(mode.item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(mode.item_id) remaining = mode.limit_amount if mode.limit_amount >= 0 else -1 cursor.execute( f""" @@ -338,7 +338,7 @@ class WPSStoreSystem(WPSAPI): now = datetime.now(timezone.utc).isoformat() backpack = Architecture.Get(WPSBackpackSystem) for mode in permanent_modes: - definition = backpack._get_definition(mode.item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(mode.item_id) cursor.execute( f""" INSERT INTO {self.SYSTEM_TABLE} ( @@ -621,7 +621,7 @@ class WPSStoreSystem(WPSAPI): for row in rows: item_id = row["item_id"] try: - definition = backpack._get_definition(item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(item_id) item_name = definition.name except Exception: item_name = item_id @@ -706,8 +706,8 @@ class WPSStoreSystem(WPSAPI): user_id: int, ) -> str: total_price = entry.price * quantity - config_api = Architecture.Get(WPSConfigAPI) - user_points = Architecture.Get(WPSConfigAPI).get_user_points(chat_id, user_id) + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) + user_points = config_api.get_user_points(user_id) if user_points < total_price: return f"❌ 积分不足,需要 {total_price} 分,当前仅有 {user_points} 分" @@ -756,7 +756,7 @@ class WPSStoreSystem(WPSAPI): matches.append(listing) continue try: - definition = backpack._get_definition(listing.item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(listing.item_id) except Exception: continue if definition.name.lower() == identifier_lower: @@ -779,8 +779,8 @@ class WPSStoreSystem(WPSAPI): return "❌ 无法购买自己上架的商品" total_price = listing.price * quantity - config_api = Architecture.Get(WPSConfigAPI) - buyer_points = config_api.get_user_points(chat_id, user_id) + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) + buyer_points = config_api.get_user_points(user_id) if buyer_points < total_price: return f"❌ 积分不足,需要 {total_price} 分,当前仅有 {buyer_points} 分" @@ -818,7 +818,7 @@ class WPSStoreSystem(WPSAPI): backpack = Architecture.Get(WPSBackpackSystem) backpack.add_item(user_id, listing.item_id, quantity) - definition = backpack._get_definition(listing.item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(listing.item_id) return ( f"✅ 成功购买玩家商品 {definition.name} × {quantity},花费 {total_price} 分\n" f"当前剩余积分:{buyer_new_points}" @@ -857,7 +857,7 @@ class WPSStoreSystem(WPSAPI): item_id = row["item_id"] backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) try: - definition = backpack._get_definition(item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(item_id) item_name = definition.name except Exception: item_name = item_id @@ -902,7 +902,7 @@ class WPSStoreSystem(WPSAPI): row = cursor.fetchone() item_id = row["item_id"] if row else identifier try: - definition = backpack._get_definition(item_id) # type: ignore[attr-defined] + definition = backpack._get_definition(item_id) return definition.item_id, definition except Exception: return None, None @@ -1002,7 +1002,7 @@ class WPSStoreBuyCommand(WPSAPI): except ValueError: return await self._send_error("❌ 购买数量必须是整数", chat_id, user_id) - store_api = Architecture.Get(WPSStoreSystem) + store_api: WPSStoreSystem = Architecture.Get(WPSStoreSystem) response = await store_api.purchase_item( chat_id=chat_id, user_id=user_id,