新增菜园系统
This commit is contained in:
@@ -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()` 参数数量不匹配异常。
|
||||||
|
- 阻碍因素:无
|
||||||
|
- 状态:未确认
|
||||||
|
|
||||||
# 最终审查
|
# 最终审查
|
||||||
(待补充)
|
(待补充)
|
||||||
|
|||||||
86
.tasks/2025-11-09_1_garden-system.md
Normal file
86
.tasks/2025-11-09_1_garden-system.md
Normal file
@@ -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
|
||||||
|
- 更改:时间展示改为到分钟级,避免显示秒数以贴合调度频率。
|
||||||
|
- 原因:输出信息更简洁,与调度粒度一致。
|
||||||
|
- 阻碍因素:无
|
||||||
|
- 状态:未确认
|
||||||
|
|
||||||
|
# 最终审查
|
||||||
|
(待补充)
|
||||||
@@ -15,6 +15,11 @@
|
|||||||
"plugin_dir": "Plugins",
|
"plugin_dir": "Plugins",
|
||||||
"alchemy_fortune_coeff": 0.03,
|
"alchemy_fortune_coeff": 0.03,
|
||||||
"store_hourly_count": 5,
|
"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",
|
"host": "0.0.0.0",
|
||||||
"port": 8000,
|
"port": 8000,
|
||||||
"verbose": false,
|
"verbose": false,
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ class WPSAlchemyGame(WPSAPI):
|
|||||||
details = []
|
details = []
|
||||||
for item_id, count in rewards.items():
|
for item_id, count in rewards.items():
|
||||||
try:
|
try:
|
||||||
definition = backpack._get_definition(item_id) # type: ignore[attr-defined]
|
definition = backpack._get_definition(item_id)
|
||||||
item_name = definition.name
|
item_name = definition.name
|
||||||
except Exception:
|
except Exception:
|
||||||
item_name = item_id
|
item_name = item_id
|
||||||
@@ -357,7 +357,7 @@ class WPSAlchemyGame(WPSAPI):
|
|||||||
item_id = row["item_id"] if row else identifier.strip()
|
item_id = row["item_id"] if row else identifier.strip()
|
||||||
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
||||||
try:
|
try:
|
||||||
return backpack._get_definition(item_id) # type: ignore[attr-defined]
|
return backpack._get_definition(item_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
23
Plugins/WPSGardenSystem/__init__.py
Normal file
23
Plugins/WPSGardenSystem/__init__.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
179
Plugins/WPSGardenSystem/garden_models.py
Normal file
179
Plugins/WPSGardenSystem/garden_models.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
222
Plugins/WPSGardenSystem/garden_plugin_base.py
Normal file
222
Plugins/WPSGardenSystem/garden_plugin_base.py
Normal file
@@ -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"]
|
||||||
78
Plugins/WPSGardenSystem/garden_plugin_harvest.py
Normal file
78
Plugins/WPSGardenSystem/garden_plugin_harvest.py
Normal file
@@ -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"]
|
||||||
72
Plugins/WPSGardenSystem/garden_plugin_plant.py
Normal file
72
Plugins/WPSGardenSystem/garden_plugin_plant.py
Normal file
@@ -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"]
|
||||||
39
Plugins/WPSGardenSystem/garden_plugin_remove.py
Normal file
39
Plugins/WPSGardenSystem/garden_plugin_remove.py
Normal file
@@ -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"]
|
||||||
86
Plugins/WPSGardenSystem/garden_plugin_steal.py
Normal file
86
Plugins/WPSGardenSystem/garden_plugin_steal.py
Normal file
@@ -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"]
|
||||||
89
Plugins/WPSGardenSystem/garden_plugin_view.py
Normal file
89
Plugins/WPSGardenSystem/garden_plugin_view.py
Normal file
@@ -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"]
|
||||||
330
Plugins/WPSGardenSystem/garden_service.py
Normal file
330
Plugins/WPSGardenSystem/garden_service.py
Normal file
@@ -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"]
|
||||||
@@ -167,7 +167,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
|
|
||||||
backpack = Architecture.Get(WPSBackpackSystem)
|
backpack = Architecture.Get(WPSBackpackSystem)
|
||||||
try:
|
try:
|
||||||
item_def = backpack._get_definition(item_id) # type: ignore[attr-defined]
|
item_def = backpack._get_definition(item_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValueError(f"Item {item_id} not registered in backpack system") from 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()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
backpack = Architecture.Get(WPSBackpackSystem)
|
backpack = Architecture.Get(WPSBackpackSystem)
|
||||||
for mode in selection:
|
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
|
remaining = mode.limit_amount if mode.limit_amount >= 0 else -1
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
f"""
|
f"""
|
||||||
@@ -338,7 +338,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
backpack = Architecture.Get(WPSBackpackSystem)
|
backpack = Architecture.Get(WPSBackpackSystem)
|
||||||
for mode in permanent_modes:
|
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(
|
cursor.execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO {self.SYSTEM_TABLE} (
|
INSERT INTO {self.SYSTEM_TABLE} (
|
||||||
@@ -621,7 +621,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
item_id = row["item_id"]
|
item_id = row["item_id"]
|
||||||
try:
|
try:
|
||||||
definition = backpack._get_definition(item_id) # type: ignore[attr-defined]
|
definition = backpack._get_definition(item_id)
|
||||||
item_name = definition.name
|
item_name = definition.name
|
||||||
except Exception:
|
except Exception:
|
||||||
item_name = item_id
|
item_name = item_id
|
||||||
@@ -706,8 +706,8 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
) -> str:
|
) -> str:
|
||||||
total_price = entry.price * quantity
|
total_price = entry.price * quantity
|
||||||
config_api = Architecture.Get(WPSConfigAPI)
|
config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI)
|
||||||
user_points = Architecture.Get(WPSConfigAPI).get_user_points(chat_id, user_id)
|
user_points = config_api.get_user_points(user_id)
|
||||||
if user_points < total_price:
|
if user_points < total_price:
|
||||||
return f"❌ 积分不足,需要 {total_price} 分,当前仅有 {user_points} 分"
|
return f"❌ 积分不足,需要 {total_price} 分,当前仅有 {user_points} 分"
|
||||||
|
|
||||||
@@ -756,7 +756,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
matches.append(listing)
|
matches.append(listing)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
definition = backpack._get_definition(listing.item_id) # type: ignore[attr-defined]
|
definition = backpack._get_definition(listing.item_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if definition.name.lower() == identifier_lower:
|
if definition.name.lower() == identifier_lower:
|
||||||
@@ -779,8 +779,8 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
return "❌ 无法购买自己上架的商品"
|
return "❌ 无法购买自己上架的商品"
|
||||||
|
|
||||||
total_price = listing.price * quantity
|
total_price = listing.price * quantity
|
||||||
config_api = Architecture.Get(WPSConfigAPI)
|
config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI)
|
||||||
buyer_points = config_api.get_user_points(chat_id, user_id)
|
buyer_points = config_api.get_user_points(user_id)
|
||||||
if buyer_points < total_price:
|
if buyer_points < total_price:
|
||||||
return f"❌ 积分不足,需要 {total_price} 分,当前仅有 {buyer_points} 分"
|
return f"❌ 积分不足,需要 {total_price} 分,当前仅有 {buyer_points} 分"
|
||||||
|
|
||||||
@@ -818,7 +818,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
backpack = Architecture.Get(WPSBackpackSystem)
|
backpack = Architecture.Get(WPSBackpackSystem)
|
||||||
backpack.add_item(user_id, listing.item_id, quantity)
|
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 (
|
return (
|
||||||
f"✅ 成功购买玩家商品 {definition.name} × {quantity},花费 {total_price} 分\n"
|
f"✅ 成功购买玩家商品 {definition.name} × {quantity},花费 {total_price} 分\n"
|
||||||
f"当前剩余积分:{buyer_new_points}"
|
f"当前剩余积分:{buyer_new_points}"
|
||||||
@@ -857,7 +857,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
item_id = row["item_id"]
|
item_id = row["item_id"]
|
||||||
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
||||||
try:
|
try:
|
||||||
definition = backpack._get_definition(item_id) # type: ignore[attr-defined]
|
definition = backpack._get_definition(item_id)
|
||||||
item_name = definition.name
|
item_name = definition.name
|
||||||
except Exception:
|
except Exception:
|
||||||
item_name = item_id
|
item_name = item_id
|
||||||
@@ -902,7 +902,7 @@ class WPSStoreSystem(WPSAPI):
|
|||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
item_id = row["item_id"] if row else identifier
|
item_id = row["item_id"] if row else identifier
|
||||||
try:
|
try:
|
||||||
definition = backpack._get_definition(item_id) # type: ignore[attr-defined]
|
definition = backpack._get_definition(item_id)
|
||||||
return definition.item_id, definition
|
return definition.item_id, definition
|
||||||
except Exception:
|
except Exception:
|
||||||
return None, None
|
return None, None
|
||||||
@@ -1002,7 +1002,7 @@ class WPSStoreBuyCommand(WPSAPI):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return await self._send_error("❌ 购买数量必须是整数", chat_id, user_id)
|
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(
|
response = await store_api.purchase_item(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user