diff --git a/Plugins/WPSCombatSystem/combat_plugin_base.py b/Plugins/WPSCombatSystem/combat_plugin_base.py index 0066186..52865aa 100644 --- a/Plugins/WPSCombatSystem/combat_plugin_base.py +++ b/Plugins/WPSCombatSystem/combat_plugin_base.py @@ -24,6 +24,17 @@ from .combat_models import ( ) from .combat_service import CombatService, get_combat_service +# 尝试导入菜园系统(可选依赖) +try: + from Plugins.WPSGardenSystem import ( + GardenCropDefinition, + GardenExtraReward, + register_crop, + ) + GARDEN_SYSTEM_AVAILABLE = True +except ImportError: + GARDEN_SYSTEM_AVAILABLE = False + logger: ProjectConfig = Architecture.Get(ProjectConfig) @@ -109,7 +120,11 @@ class WPSCombatBase(WPSAPI): for item_id, (name, tier, desc) in ADVENTURE_SEEDS.items(): self._safe_register_item(backpack, item_id, name, tier, desc) # 种子只能通过冒险获得 - + + # 5.1. 注册冒险种子到菜园系统(如果可用) + if GARDEN_SYSTEM_AVAILABLE: + self._register_adventure_seeds_to_garden() + # 6. 恢复过期任务和超时战斗 try: service = self.service() @@ -189,5 +204,77 @@ class WPSCombatBase(WPSAPI): return price + skill_bonus + def _register_adventure_seeds_to_garden(self) -> None: + """注册冒险种子到菜园系统""" + if not GARDEN_SYSTEM_AVAILABLE: + return + + try: + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + + # 战斗之花种子(EPIC) + battle_flower_crop = GardenCropDefinition( + seed_id="combat_seed_battle_flower", + fruit_id="combat_fruit_battle_flower", + display_name="战斗之花", + tier="epic", + growth_minutes=240, # 4小时,比稀有树木更长 + seed_price=300, # 比稀有树木更贵 + base_yield=2, # 产量较低,体现稀有性 + extra_reward=GardenExtraReward( + kind="points", + payload={"min": 500, "max": 2000}, + base_rate=0.35, # 较低的基础概率,体现稀有性 + ), + wine_item_id=None, # 暂不设置果酒 + wine_tier=None, + ) + register_crop(battle_flower_crop) + # 注册果实到背包系统 + self._safe_register_item( + backpack, + battle_flower_crop.fruit_id, + "战斗之花的果实", + BackpackItemTier.EPIC, + "战斗之花成熟后的果实,可食用或售出换取积分。", + ) + + # 胜利之树种子(LEGENDARY) + victory_tree_crop = GardenCropDefinition( + seed_id="combat_seed_victory_tree", + fruit_id="combat_fruit_victory_tree", + display_name="胜利之树", + tier="legendary", + growth_minutes=480, # 8小时,最长的生长时间 + seed_price=800, # 最贵的种子 + base_yield=1, # 最低产量,体现传说级稀有性 + extra_reward=GardenExtraReward( + kind="points", + payload={"min": 2000, "max": 5000}, + base_rate=0.25, # 最低的基础概率 + ), + wine_item_id=None, # 暂不设置果酒 + wine_tier=None, + ) + register_crop(victory_tree_crop) + # 注册果实到背包系统 + self._safe_register_item( + backpack, + victory_tree_crop.fruit_id, + "胜利之树的果实", + BackpackItemTier.LEGENDARY, + "胜利之树成熟后的果实,可食用或售出换取积分。", + ) + + logger.Log( + "Info", + f"{ConsoleFrontColor.GREEN}成功注册 {len(ADVENTURE_SEEDS)} 种冒险种子到菜园系统{ConsoleFrontColor.RESET}" + ) + except Exception as e: + logger.Log( + "Warning", + f"{ConsoleFrontColor.YELLOW}注册冒险种子到菜园系统时出错: {e}{ConsoleFrontColor.RESET}" + ) + __all__ = ["WPSCombatBase"] diff --git a/Plugins/WPSGardenSystem/__init__.py b/Plugins/WPSGardenSystem/__init__.py index 1e0340d..ac725e0 100644 --- a/Plugins/WPSGardenSystem/__init__.py +++ b/Plugins/WPSGardenSystem/__init__.py @@ -2,6 +2,7 @@ from .garden_models import ( GARDEN_CROPS, GardenCropDefinition, GardenExtraReward, + register_crop, ) from .garden_service import GardenService from .garden_plugin_view import WPSGardenView @@ -15,6 +16,7 @@ __all__ = [ "GardenExtraReward", "GARDEN_CROPS", "GardenService", + "register_crop", "WPSGardenView", "WPSGardenPlant", "WPSGardenHarvest", diff --git a/Plugins/WPSGardenSystem/garden_models.py b/Plugins/WPSGardenSystem/garden_models.py index e6a13dd..4e336e0 100644 --- a/Plugins/WPSGardenSystem/garden_models.py +++ b/Plugins/WPSGardenSystem/garden_models.py @@ -4,11 +4,14 @@ from __future__ import annotations from typing import Dict, List, Tuple +from PWF.Convention.Runtime.Config import * 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 +# 注意:延迟导入 GardenConfig 避免循环依赖 + # Shared logger/config _config: ProjectConfig = Architecture.Get(ProjectConfig) _config.SaveProperties() @@ -40,12 +43,13 @@ class GardenCropDefinition(BaseModel): allow_mutation = False -GARDEN_CONFIG_DEFAULTS: Dict[str, int | float] = { +GARDEN_CONFIG_DEFAULTS: Dict[str, int | float | str] = { "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, + "garden_crops_config_path": "Plugins/garden_crops.json", } for key, value in GARDEN_CONFIG_DEFAULTS.items(): @@ -145,12 +149,230 @@ RARE_TREE_CROPS: Tuple[GardenCropDefinition, ...] = ( ), ) -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) -} +def load_crops_from_config(config_path: str) -> Dict[str, GardenCropDefinition]: + """从配置文件加载作物定义 + + Args: + config_path: 配置文件路径 + + Returns: + 作物字典,seed_id -> GardenCropDefinition + """ + import json + from PWF.Convention.Runtime.File import ToolFile + + try: + config_file = ToolFile(config_path) + if not config_file.Exists(): + _config.Log("Warning", f"作物配置文件不存在: {config_path}") + return {} + + # 读取配置文件 + with open(config_file.GetFullPath(), 'r', encoding='utf-8') as f: + data = json.load(f) + + # 验证版本 + version = data.get("version", "1.0") + if version != "1.0": + _config.Log("Warning", f"不支持的配置文件版本: {version},期望 1.0") + return {} + + crops_data = data.get("crops", {}) + crops_dict = {} + + # 处理所有作物类别 + for category, crop_list in crops_data.items(): + for crop_data in crop_list: + try: + # 构建 GardenCropDefinition 对象 + extra_reward_data = crop_data.get("extra_reward", {}) + extra_reward = GardenExtraReward( + kind=extra_reward_data.get("kind", "points"), + payload=extra_reward_data.get("payload", {}), + base_rate=extra_reward_data.get("base_rate", 0.0) + ) + + crop = GardenCropDefinition( + seed_id=crop_data["seed_id"], + fruit_id=crop_data["fruit_id"], + display_name=crop_data["display_name"], + tier=crop_data["tier"], + growth_minutes=crop_data["growth_minutes"], + seed_price=crop_data["seed_price"], + base_yield=crop_data["base_yield"], + extra_reward=extra_reward, + extra_item_id=crop_data.get("extra_item_id"), + wine_item_id=crop_data.get("wine_item_id"), + wine_tier=crop_data.get("wine_tier") + ) + + crops_dict[crop.seed_id] = crop + + except KeyError as e: + _config.Log("Warning", f"作物配置缺少必要字段: {e}, 跳过此作物") + continue + except Exception as e: + _config.Log("Warning", f"解析作物配置失败: {e}, 跳过此作物") + continue + + _config.Log("Info", f"从配置文件加载了 {len(crops_dict)} 种作物") + return crops_dict + + except json.JSONDecodeError as e: + _config.Log("Warning", f"配置文件JSON格式错误: {e}") + return {} + except Exception as e: + _config.Log("Warning", f"加载作物配置文件失败: {e}") + return {} + + +# 延迟初始化作物字典,避免循环导入 +_GARDEN_CROPS_CACHE: Optional[Dict[str, GardenCropDefinition]] = None +_GARDEN_FRUITS_CACHE: Optional[Dict[str, GardenCropDefinition]] = None + +def _initialize_crops() -> Dict[str, GardenCropDefinition]: + """延迟初始化作物字典""" + global _GARDEN_CROPS_CACHE + if _GARDEN_CROPS_CACHE is None: + # 延迟导入避免循环依赖 + from .garden_service import GardenConfig + config = GardenConfig.load() + config_crops = load_crops_from_config(config.crops_config_path) + + if config_crops: + _GARDEN_CROPS_CACHE = config_crops + else: + # fallback到硬编码定义 + _config.Log("Info", "使用硬编码作物定义作为fallback") + _GARDEN_CROPS_CACHE = {crop.seed_id: crop for crop in (*COMMON_HERB_CROPS, *RARE_TREE_CROPS)} + return _GARDEN_CROPS_CACHE + +def _initialize_fruits() -> Dict[str, GardenCropDefinition]: + """延迟初始化果实字典""" + global _GARDEN_FRUITS_CACHE + if _GARDEN_FRUITS_CACHE is None: + crops = _initialize_crops() + _GARDEN_FRUITS_CACHE = {crop.fruit_id: crop for crop in crops.values()} + return _GARDEN_FRUITS_CACHE + + +def register_crop(crop: GardenCropDefinition, *, overwrite: bool = False) -> None: + """运行时注册作物定义到菜园系统 + + 允许外部系统(如冒险系统)动态注册种子,使其可以在菜园中种植。 + + Args: + crop: 作物定义对象 + overwrite: 如果为True,允许覆盖已存在的作物;如果为False,重复注册会记录警告但不覆盖 + + Raises: + ValueError: 如果种子ID或果实ID已存在且overwrite=False + """ + # 初始化缓存(如果还未初始化) + crops_dict = _initialize_crops() + fruits_dict = _initialize_fruits() + + if crop.seed_id in crops_dict: + if overwrite: + old_crop = crops_dict[crop.seed_id] + _config.Log( + "Info", + f"{ConsoleFrontColor.YELLOW}覆盖已存在的种子定义: {crop.seed_id} ({old_crop.display_name} -> {crop.display_name}){ConsoleFrontColor.RESET}" + ) + else: + _config.Log( + "Warning", + f"{ConsoleFrontColor.YELLOW}种子 {crop.seed_id} ({crop.display_name}) 已存在,跳过注册。如需覆盖,请设置 overwrite=True{ConsoleFrontColor.RESET}" + ) + return + + if crop.fruit_id in fruits_dict: + if overwrite: + _config.Log( + "Info", + f"{ConsoleFrontColor.YELLOW}覆盖已存在的果实定义: {crop.fruit_id}{ConsoleFrontColor.RESET}" + ) + else: + _config.Log( + "Warning", + f"{ConsoleFrontColor.YELLOW}果实 {crop.fruit_id} 已存在,跳过注册。如需覆盖,请设置 overwrite=True{ConsoleFrontColor.RESET}" + ) + return + + # 更新缓存 + global _GARDEN_CROPS_CACHE, _GARDEN_FRUITS_CACHE + if _GARDEN_CROPS_CACHE is None: + _GARDEN_CROPS_CACHE = {} + if _GARDEN_FRUITS_CACHE is None: + _GARDEN_FRUITS_CACHE = {} + + _GARDEN_CROPS_CACHE[crop.seed_id] = crop + _GARDEN_FRUITS_CACHE[crop.fruit_id] = crop + + _config.Log( + "Info", + f"{ConsoleFrontColor.GREEN}成功注册作物: {crop.display_name} (种子ID: {crop.seed_id}, 果实ID: {crop.fruit_id}){ConsoleFrontColor.RESET}" + ) + + +# 创建只读代理类,提供延迟初始化的字典访问 +class _GardenCropsProxy: + """代理类,提供只读的作物字典访问""" + + def __getitem__(self, key): + return _initialize_crops()[key] + + def __iter__(self): + return iter(_initialize_crops()) + + def __len__(self): + return len(_initialize_crops()) + + def __contains__(self, key): + return key in _initialize_crops() + + def keys(self): + return _initialize_crops().keys() + + def values(self): + return _initialize_crops().values() + + def items(self): + return _initialize_crops().items() + + def get(self, key, default=None): + return _initialize_crops().get(key, default) + +class _GardenFruitsProxy: + """代理类,提供只读的果实字典访问""" + + def __getitem__(self, key): + return _initialize_fruits()[key] + + def __iter__(self): + return iter(_initialize_fruits()) + + def __len__(self): + return len(_initialize_fruits()) + + def __contains__(self, key): + return key in _initialize_fruits() + + def keys(self): + return _initialize_fruits().keys() + + def values(self): + return _initialize_fruits().values() + + def items(self): + return _initialize_fruits().items() + + def get(self, key, default=None): + return _initialize_fruits().get(key, default) + +# 导出只读代理对象 +GARDEN_CROPS = _GardenCropsProxy() +GARDEN_FRUITS = _GardenFruitsProxy() GARDEN_MISC_ITEMS = { "garden_item_rot_fruit": { @@ -193,4 +415,6 @@ __all__ = [ "GARDEN_FRUITS", "GARDEN_MISC_ITEMS", "get_garden_db_models", + "load_crops_from_config", + "register_crop", ] diff --git a/Plugins/WPSGardenSystem/garden_plugin_base.py b/Plugins/WPSGardenSystem/garden_plugin_base.py index ac931b2..98e6464 100644 --- a/Plugins/WPSGardenSystem/garden_plugin_base.py +++ b/Plugins/WPSGardenSystem/garden_plugin_base.py @@ -65,7 +65,14 @@ class WPSGardenBase(WPSAPI): 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 + # 支持所有tier等级:common, rare, epic, legendary + tier_map = { + "common": BackpackItemTier.COMMON, + "rare": BackpackItemTier.RARE, + "epic": BackpackItemTier.EPIC, + "legendary": BackpackItemTier.LEGENDARY, + } + tier = tier_map.get(crop.tier.lower(), BackpackItemTier.RARE) seed_desc = f"{crop.display_name}的种子,可在菜园种植获取相应作物。" fruit_desc = f"{crop.display_name}成熟后的果实,可食用或售出换取积分。" self._safe_register_item(backpack, crop.seed_id, seed_name, tier, seed_desc) diff --git a/Plugins/WPSGardenSystem/garden_service.py b/Plugins/WPSGardenSystem/garden_service.py index 0ad76ed..ceda423 100644 --- a/Plugins/WPSGardenSystem/garden_service.py +++ b/Plugins/WPSGardenSystem/garden_service.py @@ -39,6 +39,7 @@ class GardenConfig(BaseModel): fortune_coeff: float theft_threshold_ratio: float seed_store_limit: int + crops_config_path: str class Config: allow_mutation = False @@ -51,6 +52,7 @@ class GardenConfig(BaseModel): 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)) + crops_config_path = str(project_config.FindItem("garden_crops_config_path", "Plugins/garden_crops.json")) project_config.SaveProperties() return cls( max_plots=max_plots, @@ -58,6 +60,7 @@ class GardenConfig(BaseModel): fortune_coeff=fortune_coeff, theft_threshold_ratio=theft_ratio, seed_store_limit=seed_store_limit, + crops_config_path=crops_config_path, ) diff --git a/Plugins/garden_crops.json b/Plugins/garden_crops.json new file mode 100644 index 0000000..cf1dae3 --- /dev/null +++ b/Plugins/garden_crops.json @@ -0,0 +1,154 @@ +{ + "version": "1.0", + "crops": { + "common_herbs": [ + { + "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": { + "kind": "points", + "payload": {"min": 10, "max": 120}, + "base_rate": 0.6 + }, + "wine_item_id": "garden_wine_mint", + "wine_tier": "rare" + }, + { + "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": { + "kind": "points", + "payload": {"min": 15, "max": 150}, + "base_rate": 0.55 + }, + "wine_item_id": "garden_wine_basil", + "wine_tier": "rare" + }, + { + "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": { + "kind": "points", + "payload": {"min": 20, "max": 180}, + "base_rate": 0.5 + }, + "wine_item_id": "garden_wine_sage", + "wine_tier": "rare" + }, + { + "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": { + "kind": "points", + "payload": {"min": 30, "max": 220}, + "base_rate": 0.45 + }, + "wine_item_id": "garden_wine_rosemary", + "wine_tier": "rare" + } + ], + "rare_trees": [ + { + "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": { + "kind": "item", + "payload": {"min": 2, "max": 6}, + "base_rate": 0.5 + }, + "extra_item_id": "garden_wood_ginkgo", + "wine_item_id": "garden_wine_ginkgo", + "wine_tier": "epic" + }, + { + "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": { + "kind": "item", + "payload": {"min": 3, "max": 8}, + "base_rate": 0.45 + }, + "extra_item_id": "garden_wood_sakura", + "wine_item_id": "garden_wine_sakura", + "wine_tier": "epic" + }, + { + "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": { + "kind": "item", + "payload": {"min": 4, "max": 10}, + "base_rate": 0.4 + }, + "extra_item_id": "garden_wood_maple", + "wine_item_id": "garden_wine_maple", + "wine_tier": "epic" + } + ], + "adventure_seeds": [ + { + "seed_id": "combat_seed_battle_flower", + "fruit_id": "combat_fruit_battle_flower", + "display_name": "战斗之花", + "tier": "epic", + "growth_minutes": 240, + "seed_price": 300, + "base_yield": 2, + "extra_reward": { + "kind": "points", + "payload": {"min": 500, "max": 2000}, + "base_rate": 0.35 + } + }, + { + "seed_id": "combat_seed_victory_tree", + "fruit_id": "combat_fruit_victory_tree", + "display_name": "胜利之树", + "tier": "legendary", + "growth_minutes": 480, + "seed_price": 800, + "base_yield": 1, + "extra_reward": { + "kind": "points", + "payload": {"min": 2000, "max": 5000}, + "base_rate": 0.25 + } + } + ] + } +}