diff --git a/.tasks/2025-11-15_2_trap-system.md b/.tasks/2025-11-15_2_trap-system.md new file mode 100644 index 0000000..933faac --- /dev/null +++ b/.tasks/2025-11-15_2_trap-system.md @@ -0,0 +1,68 @@ +# 背景 +文件名: 2025-11-15_2_trap-system.md +创建于: 2025-11-15_16:41:02 +创建者: ASUS +主分支: main +任务分支: (不需要创建) +Yolo模式: Off + +# 任务描述 +在菜园系统中加入陷阱功能,支持多种陷阱物品: +- 可以在某个地块放置陷阱(如防盗网等) +- 陷阱有不同等级,具有不同的触发概率、罚金金额、禁止偷盗时长和消息内容 +- 偷盗者有概率触发陷阱,触发后立即缴纳罚金并被禁止偷盗一定时间 +- 陷阱物品可以通过炼金合成获得,使用已有的矿物和木材作为材料 + +# 项目概览 +菜园系统基于PWF插件体系,依赖现有WPSConfigSystem、WPSBackpackSystem、WPSStoreSystem、WPSFortuneSystem以及WPSAlchemyGame。 + +# 分析 +- 数据库扩展:需要在 `garden_plots` 表中添加 `trap_item_id` 和 `trap_config` 字段来存储陷阱信息 +- 新增 `garden_theft_ban` 表记录用户偷盗禁令(user_id, banned_until) +- 陷阱定义:需要设计多个等级的陷阱物品,包含触发概率、罚金、禁止时长、消息等属性 +- 物品注册:在 `garden_plugin_base.py` 的 `wake_up` 中注册陷阱物品 +- 炼金配方:注册陷阱的炼金合成配方,使用矿物和木材作为材料 +- 服务层扩展:`GardenService` 需要添加 `place_trap`、`_check_trap`、`_is_theft_banned`、`_ban_theft` 等方法 +- 修改偷盗逻辑:在 `steal` 方法中检查禁令和陷阱触发 +- 插件层:创建 `garden_plugin_trap.py` 实现放置陷阱指令 + +# 提议的解决方案 +- 设计3-4个等级的陷阱物品(普通、稀有、史诗、传说),每个等级有不同的效果 +- 使用矿物(矿石、宝石、水晶、精华)和木材(银杏、樱花、红枫)合成不同等级的陷阱 +- 陷阱触发后立即扣除罚金并设置禁止偷盗时间,同时发送陷阱消息给偷盗者 +- 禁止偷盗期间用户无法偷取任何地块的作物 + +# 当前执行步骤:"已完成实现" + +# 任务进度 +- 2025-11-15_16:43:34 + - 已修改: + - Plugins/WPSGardenSystem/garden_models.py: 添加 GardenTrapDefinition 类和4种陷阱定义,扩展数据库模型 + - Plugins/WPSGardenSystem/garden_service.py: 添加陷阱相关方法(place_trap, remove_trap, _check_trap, _is_theft_banned, _ban_theft),修改 steal 方法集成陷阱检查 + - Plugins/WPSGardenSystem/garden_plugin_steal.py: 集成陷阱触发逻辑,扣除罚金并发送消息 + - Plugins/WPSGardenSystem/garden_plugin_trap.py: 创建放置/移除陷阱的插件 + - Plugins/WPSGardenSystem/garden_plugin_base.py: 在 wake_up 中注册陷阱物品和炼金配方 + - 更改: + - 实现了完整的陷阱系统,包括4个等级的陷阱(防盗网、荆棘陷阱、魔法结界、传奇守护) + - 每个陷阱有不同的触发概率、罚金、禁止时长和消息内容 + - 陷阱可以通过炼金合成获得,使用矿物和木材作为材料 + - 偷盗时会检查禁令和陷阱,触发后立即扣除罚金并设置禁止偷盗时间 + - 添加了 garden_theft_ban 表记录用户偷盗禁令 + - 原因:实现用户需求的陷阱功能 + - 阻碍因素:无 + - 状态:未确认 + +- 2025-11-15_16:46:13 + - 已修改: + - Plugins/WPSGardenSystem/garden_models.py: 为 GardenTrapDefinition 添加 durability 字段,设置各等级陷阱的耐久度(普通3次、稀有5次、史诗8次、传说15次) + - Plugins/WPSGardenSystem/garden_service.py: 在 _check_trap 中添加耐久度检查和减少逻辑,耐久度归零时自动移除陷阱;在 place_trap 中设置初始耐久度 + - Plugins/WPSGardenSystem/garden_plugin_steal.py: 在消息中显示陷阱剩余耐久度,耐久度耗尽时提示 + - Plugins/WPSGardenSystem/garden_plugin_trap.py: 在放置陷阱消息中显示耐久度 + - Plugins/WPSGardenSystem/garden_plugin_base.py: 在菜园概览中显示陷阱信息及剩余耐久度 + - 更改:实现了陷阱耐久度系统,每次触发后减少耐久度,耐久度归零时自动移除陷阱 + - 原因:满足用户需求,让陷阱具有使用次数限制 + - 阻碍因素:无 + - 状态:未确认 + +# 最终审查 + diff --git a/Plugins/WPSGardenSystem/garden_models.py b/Plugins/WPSGardenSystem/garden_models.py index 4e336e0..afe715f 100644 --- a/Plugins/WPSGardenSystem/garden_models.py +++ b/Plugins/WPSGardenSystem/garden_models.py @@ -26,6 +26,22 @@ class GardenExtraReward(BaseModel): allow_mutation = False +class GardenTrapDefinition(BaseModel): + """陷阱物品定义""" + item_id: str + display_name: str + tier: str # common / rare / epic / legendary + description: str + trigger_rate: float = Field(..., ge=0.0, le=1.0, description="触发概率") + fine_points: int = Field(..., ge=0, description="罚金积分") + ban_hours: int = Field(..., ge=0, description="禁止偷盗时长(小时)") + durability: int = Field(..., ge=1, description="耐久度(可触发次数)") + trigger_message: str = Field(..., description="触发时发送给偷盗者的消息") + + class Config: + allow_mutation = False + + class GardenCropDefinition(BaseModel): seed_id: str fruit_id: str @@ -382,6 +398,57 @@ GARDEN_MISC_ITEMS = { } } +# 陷阱物品定义 +GARDEN_TRAPS: Tuple[GardenTrapDefinition, ...] = ( + GardenTrapDefinition( + item_id="garden_trap_net", + display_name="防盗网", + tier="common", + description="基础防护陷阱,50%概率触发,对偷盗者造成小额罚金并短时禁止偷盗。", + trigger_rate=0.5, + fine_points=1000, + ban_hours=12, + durability=1, # 普通陷阱1次使用 + trigger_message="🕸️ 你触发了防盗网!被罚款 {fine} 分,并且在 {hours} 小时内无法继续偷盗。", + ), + GardenTrapDefinition( + item_id="garden_trap_thorn", + display_name="荆棘陷阱", + tier="rare", + description="带刺的防护陷阱,60%概率触发,造成中等罚金并禁止偷盗更长时间。", + trigger_rate=0.6, + fine_points=2500, + ban_hours=24, + durability=2, # 稀有陷阱2次使用 + trigger_message="🌵 你踩到了荆棘陷阱!被罚款 {fine} 分,并且在 {hours} 小时内无法继续偷盗。", + ), + GardenTrapDefinition( + item_id="garden_trap_magic", + display_name="魔法结界", + tier="epic", + description="强大的魔法防护,70%概率触发,造成高额罚金并长时间禁止偷盗。", + trigger_rate=0.7, + fine_points=5000, + ban_hours=48, + durability=3, # 史诗陷阱4次使用 + trigger_message="✨ 你触碰到了魔法结界!被罚款 {fine} 分,并且在 {hours} 小时内无法继续偷盗。", + ), + GardenTrapDefinition( + item_id="garden_trap_legend", + display_name="传奇守护", + tier="legendary", + description="传说级的防护装置,80%概率触发,造成巨额罚金并长期禁止偷盗。", + trigger_rate=0.8, + fine_points=10000, + ban_hours=72, + durability=4, # 传说陷阱4次使用 + trigger_message="⚡ 你惊醒了传奇守护!被罚款 {fine} 分,并且在 {hours} 小时内无法继续偷盗。", + ), +) + +# 陷阱物品字典(item_id -> GardenTrapDefinition) +GARDEN_TRAPS_DICT: Dict[str, GardenTrapDefinition] = {trap.item_id: trap for trap in GARDEN_TRAPS} + def get_garden_db_models() -> List[DatabaseModel]: return [ @@ -402,18 +469,32 @@ def get_garden_db_models() -> List[DatabaseModel]: "remaining_fruit": "INTEGER NOT NULL", "theft_users": "TEXT DEFAULT '[]'", "scheduled_task_id": "INTEGER", + "trap_item_id": "TEXT", + "trap_config": "TEXT", + "trap_durability": "INTEGER DEFAULT 0", "PRIMARY KEY (user_id, plot_index)": "", }, ), + DatabaseModel( + table_name="garden_theft_ban", + column_defs={ + "user_id": "INTEGER NOT NULL", + "banned_until": "TEXT NOT NULL", + "PRIMARY KEY (user_id)": "", + }, + ), ] __all__ = [ "GardenCropDefinition", "GardenExtraReward", + "GardenTrapDefinition", "GARDEN_CROPS", "GARDEN_FRUITS", "GARDEN_MISC_ITEMS", + "GARDEN_TRAPS", + "GARDEN_TRAPS_DICT", "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 8f8fe52..a007350 100644 --- a/Plugins/WPSGardenSystem/garden_plugin_base.py +++ b/Plugins/WPSGardenSystem/garden_plugin_base.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from typing import Any, Dict, List, Optional, Sequence, Type, Union, override +from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union, override from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig @@ -23,6 +23,7 @@ from .garden_models import ( GARDEN_CROPS, GARDEN_FRUITS, GARDEN_MISC_ITEMS, + GARDEN_TRAPS, GardenCropDefinition, get_garden_db_models, ) @@ -291,6 +292,59 @@ class WPSGardenBase(WPSAPI): BackpackItemTier.COMMON, meta.get("description", ""), ) + + # 注册陷阱物品 + for trap in GARDEN_TRAPS: + trap_tier = tier_map.get(trap.tier.lower(), BackpackItemTier.RARE) + self._safe_register_item( + backpack, + trap.item_id, + trap.display_name, + trap_tier, + trap.description, + ) + + # 注册陷阱的炼金合成配方 + # 获取稀有树木的木材ID + ginkgo_crop = GARDEN_CROPS.get("garden_seed_ginkgo") + sakura_crop = GARDEN_CROPS.get("garden_seed_sakura") + maple_crop = GARDEN_CROPS.get("garden_seed_maple") + + # 防盗网:矿石 + 银杏木材 + 樱花木材 + if ginkgo_crop and sakura_crop and ginkgo_crop.extra_item_id and sakura_crop.extra_item_id: + self._safe_register_trap_recipe( + alchemy, + ("combat_material_ore", ginkgo_crop.extra_item_id, sakura_crop.extra_item_id), + "garden_trap_net", + 0.75, # 75%成功率 + ) + + # 荆棘陷阱:宝石 + 红枫木材 + 银杏木材 + if maple_crop and ginkgo_crop and maple_crop.extra_item_id and ginkgo_crop.extra_item_id: + self._safe_register_trap_recipe( + alchemy, + ("combat_material_gem", maple_crop.extra_item_id, ginkgo_crop.extra_item_id), + "garden_trap_thorn", + 0.70, # 70%成功率 + ) + + # 魔法结界:水晶 + 两种木材 + if ginkgo_crop and sakura_crop and ginkgo_crop.extra_item_id and sakura_crop.extra_item_id: + self._safe_register_trap_recipe( + alchemy, + ("combat_material_crystal", ginkgo_crop.extra_item_id, sakura_crop.extra_item_id), + "garden_trap_magic", + 0.65, # 65%成功率 + ) + + # 传奇守护:精华 + 水晶 + 红枫木材 + if maple_crop and maple_crop.extra_item_id: + self._safe_register_trap_recipe( + alchemy, + ("combat_material_essence", "combat_material_crystal", maple_crop.extra_item_id), + "garden_trap_legend", + 0.60, # 60%成功率 + ) logger.Log( "Info", @@ -393,6 +447,24 @@ class WPSGardenBase(WPSAPI): ) except Exception: pass + + def _safe_register_trap_recipe( + self, + alchemy: WPSAlchemyGame, + materials: Tuple[str, str, str], + result_item_id: str, + success_rate: float, + ) -> None: + """注册陷阱的炼金合成配方""" + try: + alchemy.register_recipe( + materials, + result_item_id, + "alchemy_ash", # 失败产物是炉灰 + success_rate, + ) + except Exception: + pass async def _clock_mark_mature(self, user_id: int, chat_id: int, plot_index: int) -> None: service = self.service() @@ -454,9 +526,16 @@ class WPSGardenBase(WPSAPI): if is_mature: remaining = plot["remaining_fruit"] theft_users = len(json.loads(plot["theft_users"])) if plot.get("theft_users") else 0 + trap_info = "" + if plot.get("trap_item_id"): + trap_durability = int(plot.get("trap_durability", 0)) + from .garden_models import GARDEN_TRAPS_DICT + trap = GARDEN_TRAPS_DICT.get(plot["trap_item_id"]) + if trap: + trap_info = f"|陷阱:{trap.display_name}({trap_durability}次)" status = f"✅ 已成熟(成熟于 {formatted_time})" lines.append( - f"- 地块 {idx}|{name}|{status}|剩余果实 {remaining}|被偷次数 {theft_users}" + f"- 地块 {idx}|{name}|{status}|剩余果实 {remaining}|被偷次数 {theft_users}{trap_info}" ) else: status = f"⌛ 生长中,预计成熟 {formatted_time}" diff --git a/Plugins/WPSGardenSystem/garden_plugin_place_trap.py b/Plugins/WPSGardenSystem/garden_plugin_place_trap.py new file mode 100644 index 0000000..c59b555 --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_place_trap.py @@ -0,0 +1,143 @@ +"""Place trap plugin for garden system.""" + +from __future__ import annotations + +from typing import Optional, Sequence + +from PWF.Convention.Runtime.Architecture import Architecture + +from Plugins.WPSAPI import GuideEntry +from Plugins.WPSBackpackSystem import WPSBackpackSystem + +from .garden_plugin_base import WPSGardenBase +from .garden_models import GARDEN_TRAPS_DICT + + +class WPSGardenPlaceTrap(WPSGardenBase): + def get_guide_subtitle(self) -> str: + return "在地块上放置防护陷阱" + + def collect_command_entries(self) -> Sequence[GuideEntry]: + return ( + GuideEntry( + title="放置陷阱", + identifier="放置陷阱 <地块序号> <陷阱物品>", + description="在地块上放置防护陷阱,当偷盗者触发时会受到惩罚。", + metadata={"别名": "place_trap"}, + icon="🪤", + details=[ + { + "type": "steps", + "items": [ + "输入地块序号和陷阱物品名称或ID。", + "系统检查地块是否存在且背包中是否有该陷阱。", + "成功放置后陷阱会在下次偷盗时生效。", + ], + }, + "陷阱有不同的触发概率、罚金和禁止时长,等级越高效果越强。", + ], + ), + ) + + def collect_guide_entries(self) -> Sequence[GuideEntry]: + return ( + { + "title": "指令格式", + "description": "`放置陷阱 <地块序号> <陷阱物品>`。", + }, + { + "title": "陷阱效果", + "description": "不同等级的陷阱具有不同的触发概率、罚金和禁止时长。", + }, + ) + + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("放置陷阱") + self.register_plugin("place_trap") + + def _resolve_trap_id(self, keyword: str) -> Optional[str]: + """解析陷阱物品ID""" + key = keyword.strip().lower() + for trap_item_id, trap in GARDEN_TRAPS_DICT.items(): + if trap_item_id.lower() == key: + return trap_item_id + if trap.display_name.lower() == key: + return trap_item_id + if trap.display_name.lower().replace("陷阱", "").replace("守护", "").replace("结界", "").replace("网", "") == key: + return trap_item_id + return None + + 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 + ) + + if not tokens[0].isdigit(): + return await self.send_markdown_message( + "❌ 指令格式:`放置陷阱 <地块序号> <陷阱物品>`", + chat_id, user_id + ) + + plot_index = int(tokens[0]) + trap_identifier = " ".join(tokens[1:]) + + trap_item_id = self._resolve_trap_id(trap_identifier) + if not trap_item_id: + return await self.send_markdown_message( + "❌ 未找到该陷阱物品,可用陷阱:\n" + + "\n".join([f"- {trap.display_name} ({trap_item_id})" + for trap_item_id, trap in GARDEN_TRAPS_DICT.items()]), + chat_id, user_id + ) + + # 检查背包中是否有该陷阱 + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + user_items = backpack.get_user_items(user_id) + owned_quantity = 0 + for item in user_items: + if item.item_id == trap_item_id: + owned_quantity = item.quantity + break + + if owned_quantity <= 0: + trap = GARDEN_TRAPS_DICT[trap_item_id] + return await self.send_markdown_message( + f"❌ 背包中没有 {trap.display_name},需要先合成获取", + chat_id, user_id + ) + + try: + self.service().place_trap( + user_id=user_id, + plot_index=plot_index, + trap_item_id=trap_item_id, + ) + # 消耗陷阱物品 + backpack.set_item_quantity(user_id, trap_item_id, owned_quantity - 1) + + trap = GARDEN_TRAPS_DICT[trap_item_id] + return await self.send_markdown_message( + f"✅ 已在地块 {plot_index} 上放置 {trap.display_name}\n" + f"- 触发概率:{trap.trigger_rate * 100:.0f}%\n" + f"- 罚金:{trap.fine_points} 分\n" + f"- 禁止时长:{trap.ban_hours} 小时\n" + f"- 耐久度:{trap.durability} 次", + chat_id, user_id + ) + except ValueError as exc: + return await self.send_markdown_message(f"❌ {exc}", chat_id, user_id) + + +__all__ = ["WPSGardenPlaceTrap"] + diff --git a/Plugins/WPSGardenSystem/garden_plugin_remove_trap.py b/Plugins/WPSGardenSystem/garden_plugin_remove_trap.py new file mode 100644 index 0000000..765eeb1 --- /dev/null +++ b/Plugins/WPSGardenSystem/garden_plugin_remove_trap.py @@ -0,0 +1,77 @@ +"""Remove trap plugin for garden system.""" + +from __future__ import annotations + +from typing import Optional, Sequence + +from Plugins.WPSAPI import GuideEntry + +from .garden_plugin_base import WPSGardenBase + + +class WPSGardenRemoveTrap(WPSGardenBase): + def get_guide_subtitle(self) -> str: + return "移除地块上的防护陷阱" + + def collect_command_entries(self) -> Sequence[GuideEntry]: + return ( + GuideEntry( + title="移除陷阱", + identifier="移除陷阱 <地块序号>", + description="移除地块上的陷阱。", + metadata={"别名": "remove_trap"}, + icon="🗑️", + details=[ + { + "type": "steps", + "items": [ + "输入地块序号。", + "系统检查地块是否存在且是否有陷阱。", + "成功移除陷阱。", + ], + }, + ], + ), + ) + + def collect_guide_entries(self) -> Sequence[GuideEntry]: + return ( + { + "title": "指令格式", + "description": "`移除陷阱 <地块序号>`。", + }, + ) + + def wake_up(self) -> None: + super().wake_up() + self.register_plugin("移除陷阱") + self.register_plugin("remove_trap") + + 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]) + try: + self.service().remove_trap(user_id=user_id, plot_index=plot_index) + return await self.send_markdown_message( + f"✅ 已移除地块 {plot_index} 上的陷阱", + chat_id, user_id + ) + except ValueError as exc: + return await self.send_markdown_message(f"❌ {exc}", chat_id, user_id) + + +__all__ = ["WPSGardenRemoveTrap"] + diff --git a/Plugins/WPSGardenSystem/garden_plugin_steal.py b/Plugins/WPSGardenSystem/garden_plugin_steal.py index 2352e41..b0f377b 100644 --- a/Plugins/WPSGardenSystem/garden_plugin_steal.py +++ b/Plugins/WPSGardenSystem/garden_plugin_steal.py @@ -9,6 +9,7 @@ from PWF.Convention.Runtime.Architecture import Architecture from PWF.CoreModules.database import get_db from Plugins.WPSAPI import GuideEntry from Plugins.WPSBackpackSystem import WPSBackpackSystem +from Plugins.WPSConfigSystem import WPSConfigAPI from .garden_plugin_base import WPSGardenBase @@ -89,12 +90,36 @@ class WPSGardenSteal(WPSGardenBase): backpack.add_item(user_id, crop.fruit_id, result["stolen_quantity"]) remaining = result["remaining"] + trap_result = result.get("trap_result") + + # 处理陷阱触发 + if trap_result: + trap = trap_result["trap"] + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) + # 扣除罚金 + current_points = config_api.get_user_points(user_id) + actual_fine = min(trap_result["fine_points"], current_points) + if actual_fine > 0: + await config_api.adjust_user_points( + chat_id, user_id, -actual_fine, f"触发陷阱罚金:{trap.display_name}" + ) + + # 发送陷阱触发消息给偷盗者 + trap_message = ( + f"# 🚨 陷阱触发\n" + f"{trap_result['trigger_message']}\n" + f"- 扣除罚金:{actual_fine} 分" + ) + await self.send_markdown_message(trap_message, chat_id, user_id) + message = ( "# 🕵️ 偷取成功\n" f"- 目标:{crop.display_name}\n" f"- 获得:{crop.display_name}的果实 × {result['stolen_quantity']}\n" f"- 目标剩余果实:{remaining}" ) + if trap_result: + message += f"\n- ⚠️ 注意:你触发了陷阱!" await self.send_markdown_message(message, chat_id, user_id) owner_chat = result.get("chat_id") @@ -104,6 +129,13 @@ class WPSGardenSteal(WPSGardenBase): f"- 你的 {crop.display_name} 被偷走了 1 个果实\n" f"- 当前剩余果实:{remaining}" ) + if trap_result: + trap = trap_result["trap"] + durability = trap_result.get("durability", 0) + if trap_result.get("durability_exhausted", False): + owner_message += f"\n- 🎯 你的{trap.display_name}成功触发了!但陷阱耐久度已耗尽并被移除。" + else: + owner_message += f"\n- 🎯 好消息:你的{trap.display_name}成功触发了!剩余耐久度:{durability}次" await self.send_markdown_message(owner_message, owner_chat, owner_id) return None diff --git a/Plugins/WPSGardenSystem/garden_service.py b/Plugins/WPSGardenSystem/garden_service.py index 63d5f92..9846a70 100644 --- a/Plugins/WPSGardenSystem/garden_service.py +++ b/Plugins/WPSGardenSystem/garden_service.py @@ -18,7 +18,9 @@ from .garden_models import ( GARDEN_CROPS, GARDEN_FRUITS, GARDEN_MISC_ITEMS, + GARDEN_TRAPS_DICT, GardenCropDefinition, + GardenTrapDefinition, get_garden_db_models, ) @@ -248,12 +250,134 @@ class GardenService: # endregion # region Steal + def _is_theft_banned(self, user_id: int) -> Tuple[bool, Optional[str]]: + """检查用户是否被禁止偷盗 + + Returns: + (是否被禁止, 如果被禁止则返回解封时间字符串,否则为None) + """ + cursor = self._db.conn.cursor() + cursor.execute( + "SELECT banned_until FROM garden_theft_ban WHERE user_id = ?", + (user_id,), + ) + row = cursor.fetchone() + if not row: + return False, None + + banned_until_str = row["banned_until"] + banned_until = _parse_local_iso(banned_until_str) + now = _local_now() + + if banned_until > now: + return True, banned_until_str + else: + # 已过期,删除记录 + cursor.execute("DELETE FROM garden_theft_ban WHERE user_id = ?", (user_id,)) + self._db.conn.commit() + return False, None + + def _ban_theft(self, user_id: int, ban_hours: int) -> str: + """禁止用户偷盗一定时间 + + Returns: + 解封时间字符串 + """ + banned_until = _local_now() + timedelta(hours=ban_hours) + banned_until_str = banned_until.isoformat() + + cursor = self._db.conn.cursor() + cursor.execute( + """ + INSERT INTO garden_theft_ban (user_id, banned_until) + VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET banned_until = excluded.banned_until + """, + (user_id, banned_until_str), + ) + self._db.conn.commit() + return banned_until_str + + def _check_trap(self, plot: Dict[str, object], thief_id: int) -> Optional[Dict[str, object]]: + """检查并触发陷阱 + + Returns: + 如果触发陷阱,返回陷阱信息字典;否则返回None + """ + trap_item_id = plot.get("trap_item_id") + if not trap_item_id: + return None + + # 检查陷阱耐久度 + trap_durability = int(plot.get("trap_durability", 0)) + if trap_durability <= 0: + return None + + trap = GARDEN_TRAPS_DICT.get(trap_item_id) + if not trap: + return None + + # 检查触发概率 + if random.random() > trap.trigger_rate: + return None + + # 触发陷阱:设置禁令 + banned_until_str = self._ban_theft(thief_id, trap.ban_hours) + + # 减少耐久度 + new_durability = trap_durability - 1 + user_id = int(plot["user_id"]) + plot_index = int(plot["plot_index"]) + + cursor = self._db.conn.cursor() + if new_durability <= 0: + # 耐久度归零,移除陷阱 + cursor.execute( + """ + UPDATE garden_plots SET trap_item_id = NULL, trap_config = NULL, trap_durability = 0 + WHERE user_id = ? AND plot_index = ? + """, + (user_id, plot_index), + ) + else: + # 更新耐久度 + cursor.execute( + """ + UPDATE garden_plots SET trap_durability = ? + WHERE user_id = ? AND plot_index = ? + """, + (new_durability, user_id, plot_index), + ) + self._db.conn.commit() + + return { + "trap": trap, + "fine_points": trap.fine_points, + "ban_hours": trap.ban_hours, + "banned_until": banned_until_str, + "durability": new_durability, + "durability_exhausted": new_durability <= 0, + "trigger_message": trap.trigger_message.format( + fine=trap.fine_points, + hours=trap.ban_hours, + ), + } + 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("目标作物尚未成熟") + + # 检查是否被禁止偷盗 + is_banned, banned_until_str = self._is_theft_banned(thief_id) + if is_banned: + banned_until = _parse_local_iso(banned_until_str) + now = _local_now() + remaining_hours = (banned_until - now).total_seconds() / 3600 + raise ValueError(f"你已被禁止偷盗,解封时间:{self.format_display_time(banned_until_str)}(剩余约{int(remaining_hours)}小时)") + crop = GARDEN_CROPS.get(plot["seed_id"]) if not crop: raise ValueError("未知作物") @@ -264,6 +388,10 @@ class GardenService: theft_users = set(json.loads(plot["theft_users"])) if thief_id in theft_users: raise ValueError("你已经偷取过该作物") + + # 检查陷阱(在偷盗之前检查) + trap_result = self._check_trap(plot, thief_id) + theft_users.add(thief_id) remaining -= 1 cursor = self._db.conn.cursor() @@ -280,12 +408,61 @@ class GardenService: ), ) self._db.conn.commit() - return { + + result = { "crop": crop, "stolen_quantity": 1, "remaining": remaining, "chat_id": plot["chat_id"], + "trap_result": trap_result, } + return result + + # endregion + + # region Trap + def place_trap(self, *, user_id: int, plot_index: int, trap_item_id: str) -> None: + """在地块上放置陷阱""" + plot = self.get_plot(user_id, plot_index) + if not plot: + raise ValueError("目标地块不存在") + + trap = GARDEN_TRAPS_DICT.get(trap_item_id) + if not trap: + raise ValueError("未知陷阱物品") + + # 构建陷阱配置(JSON格式) + trap_config = json.dumps({ + "item_id": trap.item_id, + "display_name": trap.display_name, + "tier": trap.tier, + }) + + cursor = self._db.conn.cursor() + cursor.execute( + """ + UPDATE garden_plots SET trap_item_id = ?, trap_config = ?, trap_durability = ? + WHERE user_id = ? AND plot_index = ? + """, + (trap_item_id, trap_config, trap.durability, user_id, plot_index), + ) + self._db.conn.commit() + + def remove_trap(self, *, user_id: int, plot_index: int) -> None: + """移除地块上的陷阱""" + plot = self.get_plot(user_id, plot_index) + if not plot: + raise ValueError("目标地块不存在") + + cursor = self._db.conn.cursor() + cursor.execute( + """ + UPDATE garden_plots SET trap_item_id = NULL, trap_config = NULL, trap_durability = 0 + WHERE user_id = ? AND plot_index = ? + """, + (user_id, plot_index), + ) + self._db.conn.commit() # endregion