56 Commits
Author SHA1 Message Date
Diuse 2f0023d468 🚑 修复了判断用户是否注册条件异常的BUG 2025-10-22 22:44:09 +08:00
Diuse 5c043bc0c8 🐛 将获取用户基本信息的地方全部转交给Player类 2025-10-22 21:58:58 +08:00
Diuse 1be91147c1 Merge branches 'Beta' and 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-10-20 23:31:13 +08:00
术樱 0024bb22f5 Merge pull request #12 from Mualamx/Beta
🎨 修改部分html,添加点券作物
2025-10-20 23:28:51 +08:00
Diuse 00948cffb1 🎨 新增Player类 更改项目文件结构 2025-10-20 23:24:33 +08:00
Mualamx 9da91ff3d4 ✨ 实现点券商店 2025-10-16 01:02:33 +08:00
Mualamx 698f803694 ✨ 添加点券商店,与种子商店分开显示对应种子 2025-10-15 01:26:33 +08:00
Mualamx fea8ca9287 🎨 修改部分html,添加点券作物 2025-10-13 00:46:12 +08:00
Diuse 7bda14ea81 Merge remote-tracking branch 'origin/main' into Beta 2025-10-12 23:30:02 +08:00
Diuse 1fda07574c ✨ 新增农场帮助指令 2025-10-12 23:27:28 +08:00
术樱 ac495ad6e9 Merge pull request #11 from Mualamx/Beta
✨ 添加数据库字段(是否为点券作物,点券作物单价);实现点券购买种子;我的种子查询页面添加点券单价。
2025-10-12 20:23:55 +08:00
Diuse 8e30a27a41 Merge branch 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-10-12 06:12:17 +08:00
Diuse e3691a2319 📝 新增农场帮助图片 2025-10-12 06:12:14 +08:00
Mualamx f62008209f 🎨 '农场币换点券'改为'点券兑换',新增兑换点券阈值赠送规则。 2025-10-12 06:07:16 +08:00
Mualamx 34d4f698dd ✨ 添加数据库字段(是否为点券作物,点券作物单价);实现点券购买种子;我的种子查询页面添加点券单价。 2025-10-12 05:15:24 +08:00
术樱 f16c1c5ab0 Merge pull request #10 from Mualamx/Beta
MyFirstBeta
2025-10-12 03:40:19 +08:00
Mualamx 4ab21b7f7b 🔥 删除后门 2025-10-12 03:35:14 +08:00
Mualamx eb937dd309 实现农场币换点券、查看当前用户点券,偷偷加后门改农场币。 2025-10-10 13:32:45 +08:00
Mualamx 4841ceb871 Merge branch 'mx' into Beta 2025-10-10 10:59:04 +08:00
Mualamx 01fceaa4fe 实现农场币换点券、查看当前用户点券,偷偷加后门改农场币。 2025-10-10 10:58:46 +08:00
Diuse 3d42c1d283 🐛 修复签到无法正确获取作物的BUG 2025-08-06 17:51:26 +08:00
Diuse aa2c5811de 🚑 修复新字段影响旧用户导致无法收获作物的BUG 2025-06-30 04:22:51 +08:00
Diuse 69ddbe4669 🚑 修复无法正常铲除荒废作物的BUG 2025-06-30 03:43:44 +08:00
Diuse 564d9858f5 🚑 修正了铲除作物会导致土地等级失效的BUG 2025-06-30 01:07:57 +08:00
Diuse ba06fd8967 🐛 修正部分指令忽视指令前缀的BUG 2025-06-29 23:35:38 +08:00
Diuse e48bdf03da 🚑 修复土地等级绘制出错的BUG 2025-06-29 22:38:51 +08:00
Diuse f4e192ff27 🐛 修复铲除荒废作物会清空地块的BUG 2025-06-29 21:31:09 +08:00
Diuse c6f24bc3c2 ✨ 新增在线查漏补缺功能 2025-06-29 01:45:24 +08:00
Diuse ca6a414ba4 update .gitignore 2025-06-06 11:06:11 +08:00
Diuse ac13ab9280 🎨 将文本内容同一更新
🐛 修复偷菜无法正常计算的BUG
2025-06-06 10:56:22 +08:00
Diuse ba2ecf19fc 🚑 修复当作物阶段计算下标错误的BUG 2025-06-03 16:24:04 +08:00
Diuse 776b408c9c Merge branch 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-06-03 15:32:27 +08:00
Diuse d42f0aa433 🐛 修复了迁移旧数据库时因枯萎作物报错的BUG 2025-06-03 15:32:14 +08:00
Diuse 04a2cc7b4d 🐛 修复了作物无法正常绘制种子状态的BUG 2025-05-30 16:07:34 +08:00
Diuse 475865f1e9 📝 更新了README文件 2025-05-30 12:19:05 +08:00
Diuse fce307cc94 🚑 关闭了Debug模式 2025-05-30 11:23:20 +08:00
Diuse db294e6998 🐛 修正作物阶段绘制错误的BUG
🐛 修复偷菜会导致偷自己的BUG
2025-05-30 11:22:32 +08:00
Diuse 1ef506d2fd ✨ 新增每天00:30检查签到文件 2025-05-29 18:20:03 +08:00
Diuse a475d0188b 🐛 修复了农场详述无法正常输出的BUG 2025-05-29 01:27:46 +08:00
Diuse 7dc55bccf1 ✨ 种子商店新增筛选功能 2025-05-28 19:48:12 +08:00
Diuse afa2259ad8 ✨ 添加签到功能
🐛 修复默认使用格林威治时间而不是北京时间的BUG
2025-05-27 18:15:11 +08:00
Diuse c131692b19 🐛 修复迁移旧版数据库失败的BUG 2025-05-26 15:10:12 +08:00
Diuse cca3dc5d4d Merge branch 'Beta' of https://github.com/Shu-Ying/zhenxun_plugin_farm into Beta 2025-05-19 00:58:19 +08:00
Diuse 2fbaf5d54f 🐛 修复当数据库文件不存在时的异常报错 2025-05-19 00:56:17 +08:00
Diuse 6d3eb5d48f 📝 新增农场详述功能 2025-05-12 17:28:33 +08:00
Diuse ff5b446b52 ✨ 新增多阶段作物特性
🐛 修复偷菜BUG
📝 新增部分作物
2025-05-09 18:20:00 +08:00
Diuse 55394e3590 🎨 将plant.json迁移至sqlite本地数据库
📝 新增部分作物
2025-05-08 17:56:45 +08:00
Diuse 0cd9b9d8db 📝 迁移部分代码结构,使文件名和代码功能更加匹配 2025-05-07 17:04:08 +08:00
Diuse 8243a1e1c8 📝 继续完善数据库 2025-04-29 18:11:09 +08:00
Diuse 6b99ec427c 📝 继续对数据库进行优化操作 2025-04-28 19:27:16 +08:00
Diuse b5334dfc01 ✨ 完成对数据库的翻新 2025-04-27 17:00:58 +08:00
Diuse 72baa5d095 Merge remote-tracking branch 'pr/main' 2025-04-27 10:56:39 +08:00
Diuse 719494e4e5 ✨ 更新数据库逻辑 2025-04-27 10:52:28 +08:00
quanquan 5d3d8a4ef1 Update command.py 2025-04-26 10:29:59 +08:00
quanquan b72190cc45 Update command.py 2025-04-26 10:25:06 +08:00
Diuse 4cd0302426 📝 开始重构数据库 2025-04-22 19:42:09 +08:00
636 changed files with 7415 additions and 1969 deletions
+197 -1
View File
@@ -1 +1,197 @@
__pycache__
/config/sign_in.json
/resource/*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the enitre vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
+82 -11
View File
@@ -1,43 +1,114 @@
<div align="center">
# 真寻农场(zhenxun_plugin_farm)
<p align="center">
<a href="./LICENSE">
<img src="https://img.shields.io/badge/license-GPL3.0-FE7D37" alt="license">
</a>
<a href="https://www.python.org">
<img src="https://img.shields.io/badge/Python-3.10%2B-blue" alt="Python">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
</a>
<a href="https://nonebot.dev/">
<img src="https://img.shields.io/badge/Nonebot-2.0.0%2B-black" alt="Nonebot">
</a>
<a href="https://github.com/zhenxun-org/zhenxun_bot">
<img src="https://img.shields.io/badge/zhenxun-0.2.4%2B-%23ECD9D3" alt="Python">
</a>
</p>
<p align="center">
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-%E7%9C%9F%E5%AF%BB%E5%86%9C%E5%9C%BA%E6%B5%8B%E8%AF%95-%23FF99CC
)](https://qm.qq.com/q/7hsOD4rOw2)
</p>
你是说可以种地对吧🤔?
</div>
---
## 目录
- [真寻农场(zhenxun\_plugin\_farm)](#真寻农场zhenxun_plugin_farm)
- [目录](#目录)
- [农场界面](#农场界面)
- [如何安装](#如何安装)
- [使用指令](#使用指令)
- [更新日志(详细):](#更新日志详细)
- [用户方面](#用户方面)
- [代码方面](#代码方面)
- [待办事宜 `Todo` 列表](#待办事宜-todo-列表)
- [关于](#关于)
- [致谢](#致谢)
- [许可证](#许可证)
---
## 农场界面
![农场界面](./resource/1.png)
---
## 如何安装
方法一(推荐):在小真寻后台的插件商店下载即可。
方法一(推荐):在小真寻后台的插件商店下载即可<br>
方法二:下载源码放在小真寻`plugin`目录下
---
## 使用指令
| 指令 | 描述 | Tip |
| --- | --- | --- |
| @小真寻 开通农场 | 首次开通农场 | |
| 我的农场 | 你的农场 | |
| 农场详述 | 农场详细信息 | |
| 我的农场币 | 查询农场币 | |
| 种子商店 [页数] | 查看种子商店 | 数量不填默认为1 |
| 种子商店 [筛选关键字] [页数] or [页数] | 查看种子商店 | 当第一个参数为非整数时,会默认进入筛选状态。页数不填默认为1 |
| 购买种子 [种子名称] [数量] | 购买种子 | 数量不填默认为1 |
| 我的种子 | 查询仓库种子 | |
| 播种 [种子名称] [数量] | 播种种子 | 数量不填默认将最大可能播种 |
| 收获 | 收获成熟作物 | |
| 铲除 | 铲除荒废作物 | |
| 我的作物 | | |
| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 作物不填默认清空仓库 数量不填默认全部 |
| @美波理 偷菜 | 偷别人的菜 | 每人每天只能偷5次 |
| 我的作物 | 你的作物 | |
| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售 |
| 偷菜 @美波理 | 偷别人的菜 | 每人每天只能偷5次 |
| 购买农场币 | 将真寻金币兑换成农场币 | 兑换比例默认为1:2 手续费默认20% |
| 更改农场名 [新的农场名] | 改名 | 农场名称无法存储特殊字符 |
| 农场签到 | 签到 | 需要注意,该项会从服务器拉取签到数据 |
| 土地升级 [地块ID] | 将土地升级,带来收益提升 | 如果土地升级时,土地有播种作物,那么将直接成熟 |
---
## 更新日志[(详细)](./log/log.md):
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## 待办事宜 `Todo` 列表
- [x] 完善我的农场图片,例如左上角显示用户数据
- [x] 完善升级数据、作物数据、作物图片
- [ ] 完善升级数据、作物数据、作物图片
- [x] 签到功能
- [x] 添加渔场功能
- [x] 增加活动、交易行功能
- [x] 增加交易行总行功能
- [x] 添加其他游戏种子素材
- [x] 想不到了,想到再说
- [x] 在线更新作物信息
- [ ] 添加渔场功能
- [ ] 增加活动、交易行功能
- [ ] 增加交易行总行功能
- [ ] 添加其他游戏种子素材
- [ ] 想不到了,想到再说
---
+49 -13
View File
@@ -1,16 +1,21 @@
from nonebot import get_driver
from nonebot.plugin import PluginMetadata
from nonebot_plugin_apscheduler import scheduler
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
from .command import diuse_farm, diuse_register, reclamation
from .config import g_pJsonManager
from .database import g_pSqlManager
from .farm.farm import g_pFarmManager
from .farm.shop import g_pShopManager
from .request import g_pRequestManager
from .core.database.database import g_pSqlManager
from .core.dbService import g_pDBService
from .core.farm import g_pFarmManager
from .core.help import g_pHelpManager
from .core.player.playerPool import g_pUserPool
from .core.shop import g_pShopManager
from .event.event import g_pEventManager
from .utils.json import g_pJsonManager
from .utils.request import g_pRequestManager
__plugin_meta__ = PluginMetadata(
name="真寻农场",
@@ -20,22 +25,26 @@ __plugin_meta__ = PluginMetadata(
指令:
at 开通农场
我的农场
农场详述
我的农场币
种子商店 [页数]
种子商店 [筛选关键字] [页数] or [页数]
购买种子 [作物/种子名称] [数量]
我的种子
播种 [作物/种子名称] [数量]
播种 [作物/种子名称] [数量] (数量不填默认将最大可能播种
收获
铲除
我的作物
出售作物 [作物/种子名称] [数量]
偷菜 at
出售作物 [作物/种子名称] [数量] (不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售
偷菜 at (每人每天只能偷5次
开垦
购买农场币 [数量] 数量为消耗金币的数量
更改农场名 [新农场名]
农场签到
土地升级 [地块ID](通过农场详述获取)
""".strip(),
extra=PluginExtraData(
author="Art_Sakura",
version="1.0",
version="1.5.2",
commands=[Command(command="我的农场")],
menu_type="群内小游戏",
configs=[
@@ -51,6 +60,12 @@ __plugin_meta__ = PluginMetadata(
help="金币兑换农场币的倍数 默认值为: 2倍",
default_value="2",
),
RegisterConfig(
key="点券兑换倍数",
value="2000",
help="农场币兑换点券的倍数 比例为2000:1",
default_value="2000",
),
RegisterConfig(
key="手续费",
value="0.2",
@@ -62,8 +77,8 @@ __plugin_meta__ = PluginMetadata(
value="http://diuse.work",
help="签到、交易行、活动等服务器地址",
default_value="http://diuse.work",
)
]
),
],
).to_dict(),
)
driver = get_driver()
@@ -72,13 +87,34 @@ driver = get_driver()
# 构造函数
@driver.on_startup
async def start():
# 初始化数据库
# 数据库加载
await g_pSqlManager.init()
# 初始化读取Json
await g_pJsonManager.init()
# 初始化数据库变量 和 加载作物数据库
await g_pDBService.init()
# 检查作物文件是否缺失 or 更新
await g_pRequestManager.initPlantDBFile()
await g_pHelpManager.createHelpImage()
# 析构函数
@driver.on_shutdown
async def shutdown():
# 单独卸载 作物数据库
await g_pDBService.cleanup()
await g_pSqlManager.cleanup()
@scheduler.scheduled_job(trigger="cron", hour=4, minute=30, id="signInFile")
async def signInFile():
try:
await g_pJsonManager.initSignInFile()
await g_pRequestManager.initPlantDBFile()
except Exception as e:
logger.error("农场定时检查出错", e=e)
+550 -103
View File
@@ -1,83 +1,145 @@
from nonebot.adapters import Event, MessageTemplate
import inspect
from nonebot.adapters import Event
from nonebot.rule import to_me
from nonebot.typing import T_State
from nonebot_plugin_alconna import (Alconna, AlconnaQuery, Args, Arparma, At,
Match, MultiVar, Option, Query, Subcommand,
on_alconna, store_true)
from nonebot_plugin_alconna import (
Alconna,
AlconnaQuery,
Args,
At,
Match,
MultiVar,
Option,
Query,
Subcommand,
on_alconna,
store_true,
)
from nonebot_plugin_uninfo import Uninfo
from nonebot_plugin_waiter import waiter
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from zhenxun.utils.depends import UserName
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.message import MessageUtils
from .database import g_pSqlManager
from .farm.farm import g_pFarmManager
from .farm.shop import g_pShopManager
async def isRegisteredByUid(uid: str) -> bool:
point = await g_pSqlManager.getUserPointByUid(uid)
if point < 0:
await MessageUtils.build_message("尚未开通农场,快at我发送 开通农场 开通吧").send()
return False
return True
from .core.dbService import g_pDBService
from .core.farm import g_pFarmManager
from .core.shop import g_pShopManager
from .utils.config import g_bSignStatus, g_sTranslation
from .utils.json import g_pJsonManager
from .utils.tool import g_pToolManager
diuse_register = on_alconna(
Alconna("开通农场"),
priority=5,
rule=to_me(),
block=True,
use_cmd_start=True,
)
@diuse_register.handle()
async def _(session: Uninfo):
async def handle_register(session: Uninfo):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is not None and await player.isRegistered():
await MessageUtils.build_message(g_sTranslation["register"]["already"]).send(
reply_to=True
)
return
user = await g_pSqlManager.getUserInfoByUid(uid)
try:
raw_name = str(session.user.name)
safe_name = g_pToolManager.sanitize_username(raw_name)
if user:
await MessageUtils.build_message("你已经有啦").send(reply_to=True)
else:
aaa = await g_pSqlManager.initUserInfoByUid(uid, str(session.user.name), 0, 100)
success = await g_pDBService.user.initUserInfo(uid, safe_name)
logger.info(f"用户 {uid} 选择的农场名称为: {raw_name} | 过滤后为: {safe_name}")
msg = (
g_sTranslation["register"]["success"].format(point=500)
if success
else g_sTranslation["register"]["error"]
)
logger.info(f"用户注册 {'成功' if success else '失败'}:{uid}")
except Exception as e:
msg = g_sTranslation["register"]["error"]
logger.error(f"注册异常 | UID:{uid} | 错误:{e}")
await MessageUtils.build_message(msg).send(reply_to=True)
await MessageUtils.build_message(str(aaa)).send(reply_to=True)
diuse_farm = on_alconna(
Alconna(
"我的农场",
Option("--all", action=store_true),
Subcommand("detail", help_text="农场详述"),
Subcommand("my-point", help_text="我的农场币"),
Subcommand("seed-shop", Args["num?", int], help_text="种子商店"),
Subcommand("seed-shop", Args["res?", MultiVar(str)], help_text="种子商店"),
Subcommand("buy-seed", Args["name?", str]["num?", int], help_text="购买种子"),
Subcommand("my-seed", help_text="我的种子"),
Subcommand("sowing", Args["name?", str]["num?", int], help_text="播种"),
Subcommand("harvest", help_text="收获"),
Subcommand("eradicate", help_text="铲除"),
Subcommand("my-plant", help_text="我的作物"),
# Subcommand("reclamation", Args["isBool?", str], help_text="开垦"),
Subcommand("lock-plant", Args["name?", str], help_text="作物加锁"),
Subcommand("unlock-plant", Args["name?", str], help_text="作物解锁"),
Subcommand("sell-plant", Args["name?", str]["num?", int], help_text="出售作物"),
Subcommand("stealing", Args["target?", At], help_text="偷菜"),
Subcommand("buy-point", Args["num?", int], help_text="购买农场币"),
#Subcommand("sell-point", Args["num?", int], help_text="转换金币")
# Subcommand("sell-point", Args["num?", int], help_text="转换金币")
Subcommand("change-name", Args["name?", str], help_text="更改农场名"),
Subcommand("sign-in", help_text="农场签到"),
Subcommand("admin-up", Args["num?", int], help_text="农场下阶段"),
Subcommand("point-to-vipPoint", Args["num?", int], help_text="点券兑换"),
Subcommand("my-vipPoint", help_text="我的点券"),
Subcommand("farm-help", help_text="农场帮助"),
Subcommand("vipSeed-shop", Args["res?", MultiVar(str)], help_text="种子商店"),
),
priority=5,
block=True,
use_cmd_start=True,
)
@diuse_farm.assign("$main")
async def _(session: Uninfo, nickname: str = UserName()):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
@diuse_farm.assign("$main")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
image = await g_pFarmManager.drawFarmByUid(uid, nickname)
image = await g_pFarmManager.drawFarmByUid(uid)
await MessageUtils.build_message(image).send(reply_to=True)
diuse_farm.shortcut(
"农场详述",
command="我的农场",
arguments=["detail"],
prefix=True,
)
@diuse_farm.assign("detail")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
info = await g_pFarmManager.drawDetailFarmByUid(uid)
await MessageUtils.alc_forward_msg(
[info], session.self_id, BotConfig.self_nickname
).send()
diuse_farm.shortcut(
"我的农场币",
command="我的农场",
@@ -85,16 +147,25 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-point")
async def _(session: Uninfo):
uid = str(session.user.id)
point = await g_pSqlManager.getUserPointByUid(uid)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
point = player.user["point"]
if point < 0:
await MessageUtils.build_message("尚未开通农场,快at我发送 开通农场 开通吧").send()
await MessageUtils.build_message(g_sTranslation["basic"]["notFarm"]).send()
return False
await MessageUtils.build_message(f"你的当前农场币为: {point}").send(reply_to=True)
await MessageUtils.build_message(
g_sTranslation["basic"]["point"].format(point=point)
).send(reply_to=True)
diuse_farm.shortcut(
"种子商店(.*?)",
@@ -103,16 +174,47 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("seed-shop")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 1)):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
@diuse_farm.assign("seed-shop")
async def _(session: Uninfo, res: Match[tuple[str, ...]]):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
image = await g_pShopManager.getSeedShopImage(num.result)
if res.result is inspect._empty:
raw = []
else:
raw = res.result
filterKey: str | int | None = None
page: int = 1
if len(raw) >= 1 and raw[0] is not None:
first = raw[0]
if isinstance(first, str) and first.isdigit():
page = int(first)
else:
filterKey = first
if (
len(raw) >= 2
and raw[1] is not None
and isinstance(raw[1], str)
and raw[1].isdigit()
):
page = int(raw[1])
if filterKey is None:
image = await g_pShopManager.getSeedShopImage(page, 0, 0)
else:
image = await g_pShopManager.getSeedShopImage(filterKey, page, 0)
await MessageUtils.build_message(image).send()
diuse_farm.shortcut(
"购买种子(?P<name>.*?)",
command="我的农场",
@@ -120,21 +222,26 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("buy-seed")
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", 1),):
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", 1)
):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要购买的种子名称"
).finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["buySeed"]["notSeed"]).finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pShopManager.buySeed(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的种子",
command="我的农场",
@@ -142,16 +249,19 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-seed")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.getUserSeedByUid(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"播种(?P<name>.*?)",
command="我的农场",
@@ -159,16 +269,20 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("sowing")
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1),):
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)
):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要播种的种子名称"
).finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["sowing"]["notSeed"]).finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.sowing(uid, name.result, num.result)
@@ -182,16 +296,19 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("harvest")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.harvest(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"铲除",
command="我的农场",
@@ -199,11 +316,13 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("eradicate")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.eradicate(uid)
@@ -217,47 +336,58 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("my-plant")
async def _(session: Uninfo):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.getUserPlantByUid(uid)
await MessageUtils.build_message(result).send(reply_to=True)
reclamation = on_alconna(
Alconna("开垦"),
priority=5,
block=True,
diuse_farm.shortcut(
"作物加锁(?P<name>)",
command="我的农场",
arguments=["lock-plant", "{name}"],
prefix=True,
)
@reclamation.handle()
async def _(session: Uninfo):
@diuse_farm.assign("lock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
condition = await g_pFarmManager.reclamationCondition(uid)
condition += "\n 回复是将执行开垦"
await MessageUtils.build_message(condition).send(reply_to=True)
result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 1)
await MessageUtils.build_message(result).send(reply_to=True)
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message("等待超时").send(reply_to=True)
return
if not resp == "是":
diuse_farm.shortcut(
"作物解锁(?P<name>)",
command="我的农场",
arguments=["unlock-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("unlock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
res = await g_pFarmManager.reclamation(uid)
await MessageUtils.build_message(res).send(reply_to=True)
result = await g_pFarmManager.lockUserPlantByUid(uid, name.result, 0)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"出售作物(?P<name>.*?)",
@@ -266,21 +396,58 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("sell-plant")
async def _(session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", 1),):
if not name.available:
await MessageUtils.build_message(
"请在指令后跟需要出售的作物名称"
).finish(reply_to=True)
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)
):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pShopManager.sellPlantByUid(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
reclamation = on_alconna(
Alconna("开垦"),
priority=5,
block=True,
use_cmd_start=True,
)
@reclamation.handle()
async def _(session: Uninfo):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
condition = await g_pFarmManager.reclamationCondition(uid)
condition += f"\n{g_sTranslation['reclamation']['confirm']}"
await MessageUtils.build_message(condition).send(reply_to=True)
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message(g_sTranslation["reclamation"]["timeOut"]).send(
reply_to=True
)
return
if not resp == "是":
return
res = await g_pFarmManager.reclamation(uid)
await MessageUtils.build_message(res).send(reply_to=True)
diuse_farm.shortcut(
"偷菜",
command="我的农场",
@@ -288,26 +455,33 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("stealing")
async def _(session: Uninfo, target: Match[At]):
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
if not target.available:
await MessageUtils.build_message("请在指令后跟需要at的人").finish(reply_to=True)
await MessageUtils.build_message(g_sTranslation["stealing"]["noTarget"]).finish(
reply_to=True
)
tar = target.result
point = await g_pSqlManager.getUserPointByUid(tar.target)
result = await g_pDBService.user.isRegistered(tar.target)
if point < 0:
await MessageUtils.build_message("目标尚未开通农场,快邀请ta开通吧").send()
if not result:
await MessageUtils.build_message(
g_sTranslation["stealing"]["targetNotFarm"]
).send()
return None
result = await g_pFarmManager.stealing(uid, tar.target)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"购买农场币(.*?)",
command="我的农场",
@@ -315,17 +489,290 @@ diuse_farm.shortcut(
prefix=True,
)
@diuse_farm.assign("buy-point")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
if num.result <= 0:
await MessageUtils.build_message(
"请在指令后跟需要购买农场币的数量"
).finish(reply_to=True)
await MessageUtils.build_message("请在指令后跟需要购买农场币的数量").finish(
reply_to=True
)
uid = str(session.user.id)
if await isRegisteredByUid(uid) == False:
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.buyPointByUid(uid, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"更改农场名(?P<name>)",
command="我的农场",
arguments=["change-name", "{name}"],
prefix=True,
)
@diuse_farm.assign("change-name")
async def _(session: Uninfo, name: Match[str]):
if not name.available:
await MessageUtils.build_message(g_sTranslation["changeName"]["noName"]).finish(
reply_to=True
)
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
player = await g_pToolManager.getPlayerByUid(uid)
if player is None:
await MessageUtils.build_message(g_sTranslation["changeName"]["error1"]).send(
reply_to=True
)
return
result = await player.updateName(name.result)
await MessageUtils.build_message(g_sTranslation["changeName"][result]).send(
reply_to=True
)
diuse_farm.shortcut(
"农场签到",
command="我的农场",
arguments=["sign-in"],
prefix=True,
)
@diuse_farm.assign("sign-in")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
# 判断签到是否正常加载
if not g_bSignStatus:
await MessageUtils.build_message(g_sTranslation["signIn"]["error"]).send()
return
toDay = g_pToolManager.dateTime().date().today()
message = ""
status = await g_pDBService.userSign.sign(uid, toDay.strftime("%Y-%m-%d"))
# 如果完成签到
if status == 1 or status == 2:
# 获取签到总天数
signDay = await g_pDBService.userSign.getUserSignCountByDate(
uid, toDay.strftime("%Y-%m")
)
exp, point = await g_pDBService.userSign.getUserSignRewardByDate(
uid, toDay.strftime("%Y-%m-%d")
)
message += g_sTranslation["signIn"]["success"].format(
day=signDay, exp=exp, num=point
)
reward = g_pJsonManager.m_pSign["continuou"].get(f"{signDay}", None)
if reward:
extraPoint = reward.get("point", 0)
extraExp = reward.get("exp", 0)
plant = reward.get("plant", {})
message += g_sTranslation["signIn"]["grandTotal"].format(
exp=extraExp, num=extraPoint
)
vipPoint = reward.get("vipPoint", 0)
if vipPoint > 0:
message += g_sTranslation["signIn"]["grandTotal1"].format(num=vipPoint)
if plant:
for key, value in plant.items():
message += g_sTranslation["signIn"]["grandTotal2"].format(
name=key, num=value
)
else:
message = g_sTranslation["signIn"]["error1"]
await MessageUtils.build_message(message).send()
soil_upgrade = on_alconna(
Alconna("土地升级", Args["index", int]),
priority=5,
block=True,
use_cmd_start=True,
)
@soil_upgrade.handle()
async def _(session: Uninfo, index: Query[int] = AlconnaQuery("index", 1)):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
condition = await g_pFarmManager.soilUpgradeCondition(uid, index.result)
await MessageUtils.build_message(condition).send(reply_to=True)
if not condition.startswith("将土地升级至:"):
return
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message(g_sTranslation["soilInfo"]["timeOut"]).send(
reply_to=True
)
return
if not resp == "是":
return
res = await g_pFarmManager.soilUpgrade(uid, index.result)
await MessageUtils.build_message(res).send(reply_to=True)
diuse_farm.shortcut(
"农场下阶段(.*?)",
command="我的农场",
arguments=["admin-up"],
prefix=True,
)
@diuse_farm.assign("admin-up")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
await g_pDBService.userSoil.nextPhase(uid, num.result)
diuse_farm.shortcut(
"点券兑换(.*?)",
command="我的农场",
arguments=["point-to-vipPoint"],
prefix=True,
)
@diuse_farm.assign("point-to-vipPoint")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
if num.result <= 0:
await MessageUtils.build_message("请在指令后跟需要购买点券的数量").finish(
reply_to=True
)
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
result = await g_pFarmManager.pointToVipPointByUid(uid, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的点券",
command="我的农场",
arguments=["my-vipPoint"],
prefix=True,
)
@diuse_farm.assign("my-vipPoint")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
await MessageUtils.build_message(
g_sTranslation["basic"]["vipPoint"].format(vipPoint=player.user["vipPoint"])
).send(reply_to=True)
diuse_farm.shortcut(
"农场帮助",
command="我的农场",
arguments=["farm-help"],
prefix=True,
)
@diuse_farm.assign("farm-help")
async def _(session: Uninfo):
savePath = DATA_PATH / "farm_res/html/help.png"
image = BuildImage(background=savePath)
await MessageUtils.build_message(image).send(reply_to=True)
diuse_farm.shortcut(
"点券商店(.*?)",
command="我的农场",
arguments=["vipSeed-shop"],
prefix=True,
)
@diuse_farm.assign("vipSeed-shop")
async def _(session: Uninfo, res: Match[tuple[str, ...]]):
uid = str(session.user.id)
player = await g_pToolManager.getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await g_pToolManager.repeat()
return
if res.result is inspect._empty:
raw = []
else:
raw = res.result
filterKey: str | int | None = None
page: int = 1
if len(raw) >= 1 and raw[0] is not None:
first = raw[0]
if isinstance(first, str) and first.isdigit():
page = int(first)
else:
filterKey = first
if (
len(raw) >= 2
and raw[1] is not None
and isinstance(raw[1], str)
and raw[1].isdigit()
):
page = int(raw[1])
if filterKey is None:
image = await g_pShopManager.getSeedShopImage(page, 0, 1)
else:
image = await g_pShopManager.getSeedShopImage(filterKey, page, 1)
await MessageUtils.build_message(image).send()
+99 -15
View File
@@ -38,50 +38,134 @@
},
"9":
{
"level": 15,
"level": 11,
"point": 19800,
"item": []
},
"10":
{
"level":3,
"point": 3000,
"level": 13,
"point": 30000,
"item": []
},
"11":
{
"level":3,
"point": 3000,
"level": 15,
"point": 50000,
"item": []
},
"12":
{
"level":3,
"point": 3000,
"level": 17,
"point": 70000,
"item": []
},
"13":
{
"level":3,
"point": 3000,
"level": 19,
"point": 90000,
"item": []
},
"14":
{
"level":3,
"point": 3000,
"level": 21,
"point": 130000,
"item": []
},
"15":
{
"level":3,
"point": 3000,
"level": 23,
"point": 150000,
"item": []
},
"16":
{
"level":3,
"point": 3000,
"level": 25,
"point": 180000,
"item": []
},
"17":
{
"level": 27,
"point": 230000,
"item": []
},
"18":
{
"level": 29,
"point": 300000,
"item": []
},
"19":
{
"level": 31,
"point": 500000,
"item": []
},
"20":
{
"level": 33,
"point": 850000,
"item": []
},
"21":
{
"level": 36,
"point": 1100000,
"item": []
},
"22":
{
"level": 39,
"point": 1300000,
"item": []
},
"23":
{
"level": 42,
"point": 1500000,
"item": []
},
"24":
{
"level": 45,
"point": 1700000,
"item": []
},
"25":
{
"level": 48,
"point": 2000000,
"item": []
},
"26":
{
"level": 51,
"point": 2200000,
"item": []
},
"27":
{
"level": 54,
"point": 2800000,
"item": []
},
"28":
{
"level": 57,
"point": 3500000,
"item": []
},
"29":
{
"level": 60,
"point": 4300000,
"item": []
},
"30":
{
"level": 70,
"point": 5200000,
"item": []
}
}
-160
View File
@@ -1,160 +0,0 @@
{
"version": 0.01,
"zhuShi":
[
"level: 解锁等级",
"limit: 限制等级 0:普通土地 1:红土地 2:黄土地 3:黑土地",
"experience: 收获经验",
"harvest: 收获数量",
"price: 单价",
"time: 成熟时间 小时",
"crop: 作物可以收几次",
"again: 再次成熟时间 单位:小时",
"phase: 阶段 目前为 成熟时间 / 阶段 来显示每阶段图片",
"general: 第一阶段是否为通用阶段素材",
"sell: 是否可以上架交易行"
],
"plant":
{
"胡萝卜":
{
"level": 0,
"limit": 0,
"experience": 18,
"harvest": 17,
"price": 21,
"time": 13,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"白萝卜":
{
"level": 0,
"limit": 0,
"experience": 15,
"harvest": 16,
"price": 17,
"time": 10,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"牧草":
{
"level": 0,
"limit": 0,
"experience": 10,
"harvest": 25,
"price": 6,
"time": 8,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"大白菜":
{
"level": 1,
"limit": 0,
"experience": 19,
"harvest": 17,
"price": 22,
"time": 14,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"大蒜":
{
"level": 1,
"limit": 0,
"experience": 19,
"harvest": 17,
"price": 22,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"水稻":
{
"level": 2,
"limit": 0,
"experience": 19,
"harvest": 18,
"price": 21,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"小麦":
{
"level": 2,
"limit": 0,
"experience": 19,
"harvest": 18,
"price": 21,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"玉米":
{
"level": 3,
"limit": 0,
"experience": 19,
"harvest": 17,
"price": 23,
"time": 14,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
},
"油菜":
{
"level": 4,
"limit": 0,
"experience": 29,
"harvest": 23,
"price": 24,
"time": 17,
"crop": 1,
"again": 0,
"phase": 5,
"general": true,
"sell": false
},
"生菜":
{
"level": 4,
"limit": 0,
"experience": 25,
"harvest": 21,
"price": 24,
"time": 19,
"crop": 1,
"again": 0,
"phase": 6,
"general": true,
"sell": false
}
}
}
+99
View File
@@ -153,5 +153,104 @@
"x": 1451,
"y": 1072
}
},
"upgrade":
{
"red": [
{ "level": 28, "point": 200000, "vipPoint": 0, "item":{}},
{ "level": 29, "point": 220000, "vipPoint": 0, "item":{}},
{ "level": 30, "point": 240000, "vipPoint": 0, "item":{}},
{ "level": 31, "point": 260000, "vipPoint": 0, "item":{}},
{ "level": 32, "point": 290000, "vipPoint": 0, "item":{}},
{ "level": 33, "point": 320000, "vipPoint": 0, "item":{}},
{ "level": 34, "point": 350000, "vipPoint": 0, "item":{}},
{ "level": 35, "point": 380000, "vipPoint": 0, "item":{}},
{ "level": 36, "point": 410000, "vipPoint": 0, "item":{}},
{ "level": 37, "point": 440000, "vipPoint": 0, "item":{}},
{ "level": 38, "point": 480000, "vipPoint": 0, "item":{}},
{ "level": 39, "point": 520000, "vipPoint": 0, "item":{}},
{ "level": 40, "point": 560000, "vipPoint": 0, "item":{}},
{ "level": 41, "point": 600000, "vipPoint": 0, "item":{}},
{ "level": 42, "point": 650000, "vipPoint": 0, "item":{}},
{ "level": 43, "point": 700000, "vipPoint": 0, "item":{}},
{ "level": 44, "point": 770000, "vipPoint": 0, "item":{}},
{ "level": 45, "point": 900000, "vipPoint": 0, "item":{}},
{ "level": 47, "point": 1500000, "vipPoint": 0, "item":{}},
{ "level": 49, "point": 2000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 55, "point": 5500000, "vipPoint": 0, "item":{}},
{ "level": 57, "point": 6800000, "vipPoint": 0, "item":{}},
{ "level": 60, "point": 10000000, "vipPoint": 0, "item":{}},
{ "level": 64, "point": 15000000, "vipPoint": 0, "item":{}},
{ "level": 68, "point": 20000000, "vipPoint": 0, "item":{}},
{ "level": 73, "point": 30000000, "vipPoint": 0, "item":{}},
{ "level": 78, "point": 50000000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 80000000, "vipPoint": 0, "item":{}}
],
"black": [
{ "level": 40, "point": 500000, "vipPoint": 0, "item":{}},
{ "level": 41, "point": 600000, "vipPoint": 0, "item":{}},
{ "level": 42, "point": 700000, "vipPoint": 0, "item":{}},
{ "level": 43, "point": 800000, "vipPoint": 0, "item":{}},
{ "level": 44, "point": 900000, "vipPoint": 0, "item":{}},
{ "level": 45, "point": 1000000, "vipPoint": 0, "item":{}},
{ "level": 46, "point": 2000000, "vipPoint": 0, "item":{}},
{ "level": 47, "point": 2200000, "vipPoint": 0, "item":{}},
{ "level": 48, "point": 2400000, "vipPoint": 0, "item":{}},
{ "level": 49, "point": 2600000, "vipPoint": 0, "item":{}},
{ "level": 50, "point": 2800000, "vipPoint": 0, "item":{}},
{ "level": 51, "point": 3000000, "vipPoint": 0, "item":{}},
{ "level": 52, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4200000, "vipPoint": 0, "item":{}},
{ "level": 54, "point": 4400000, "vipPoint": 0, "item":{}},
{ "level": 55, "point": 4600000, "vipPoint": 0, "item":{}},
{ "level": 56, "point": 4800000, "vipPoint": 0, "item":{}},
{ "level": 57, "point": 5000000, "vipPoint": 0, "item":{}},
{ "level": 59, "point": 6000000, "vipPoint": 0, "item":{}},
{ "level": 61, "point": 6200000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 67, "point": 6800000, "vipPoint": 0, "item":{}},
{ "level": 69, "point": 7000000, "vipPoint": 0, "item":{}},
{ "level": 72, "point": 10000000, "vipPoint": 0, "item":{}},
{ "level": 76, "point": 16000000, "vipPoint": 0, "item":{}},
{ "level": 80, "point": 27000000, "vipPoint": 0, "item":{}},
{ "level": 85, "point": 43000000, "vipPoint": 0, "item":{}},
{ "level": 90, "point": 65000000, "vipPoint": 0, "item":{}},
{ "level": 95, "point": 93000000, "vipPoint": 0, "item":{}}
],
"gold": [
{ "level": 58, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 59, "point": 4200000, "vipPoint": 0, "item":{}},
{ "level": 60, "point": 4400000, "vipPoint": 0, "item":{}},
{ "level": 61, "point": 4600000, "vipPoint": 0, "item":{}},
{ "level": 62, "point": 4800000, "vipPoint": 0, "item":{}},
{ "level": 63, "point": 5000000, "vipPoint": 0, "item":{}},
{ "level": 64, "point": 5200000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 5400000, "vipPoint": 0, "item":{}},
{ "level": 66, "point": 5600000, "vipPoint": 0, "item":{}},
{ "level": 67, "point": 5800000, "vipPoint": 0, "item":{}},
{ "level": 68, "point": 6000000, "vipPoint": 0, "item":{}},
{ "level": 69, "point": 6200000, "vipPoint": 0, "item":{}},
{ "level": 70, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 71, "point": 7000000, "vipPoint": 0, "item":{}},
{ "level": 72, "point": 7400000, "vipPoint": 0, "item":{}},
{ "level": 73, "point": 7800000, "vipPoint": 0, "item":{}},
{ "level": 74, "point": 8200000, "vipPoint": 0, "item":{}},
{ "level": 75, "point": 8600000, "vipPoint": 0, "item":{}},
{ "level": 77, "point": 9500000, "vipPoint": 0, "item":{}},
{ "level": 79, "point": 10400000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 12200000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 12200000, "vipPoint": 0, "item":{}},
{ "level": 85, "point": 13100000, "vipPoint": 0, "item":{}},
{ "level": 87, "point": 14000000, "vipPoint": 0, "item":{}},
{ "level": 90, "point": 16000000, "vipPoint": 0, "item":{}},
{ "level": 94, "point": 20000000, "vipPoint": 0, "item":{}},
{ "level": 98, "point": 28000000, "vipPoint": 0, "item":{}},
{ "level": 103, "point": 44000000, "vipPoint": 0, "item":{}},
{ "level": 108, "point": 76000000, "vipPoint": 0, "item":{}},
{ "level": 113, "point": 1080000000, "vipPoint": 0, "item":{}}
]
}
}
+108
View File
@@ -0,0 +1,108 @@
class ActivityManager:
"""活动管理器"""
def __init__(self):
self.activities = {} # 活动ID -> 活动配置
self.active_activities = set() # 当前活跃的活动ID
self.effect_handlers = {} # 效果类型 -> 处理器
# 注册效果处理器
self._register_handlers()
def _register_handlers(self):
"""注册所有效果处理器"""
self.effect_handlers[EffectType.MULTIPLIER] = MultiplierHandler()
self.effect_handlers[EffectType.FIXED_BONUS] = FixedBonusHandler()
self.effect_handlers[EffectType.BUFF_APPLICATION] = BuffApplicationHandler()
self.effect_handlers[EffectType.QUEST_TRIGGER] = QuestTriggerHandler()
def load_activities_from_config(self, config_path: str):
"""从配置文件加载活动"""
with open(config_path, encoding="utf-8") as f:
config = json.load(f)
for activity_data in config["activities"]:
activity = Activity(activity_data)
self.activities[activity.id] = activity
print(f"加载活动: {activity.name}")
def update_activity_status(self):
"""更新活动状态(定时调用)"""
now = datetime.now()
for activity in self.activities.values():
is_active = activity.start_time <= now <= activity.end_time
if is_active and activity.id not in self.active_activities:
# 活动开始
self.active_activities.add(activity.id)
print(f"活动开始: {activity.name}")
elif not is_active and activity.id in self.active_activities:
# 活动结束
self.active_activities.remove(activity.id)
print(f"活动结束: {activity.name}")
def get_active_activities(self, activity_type: ActivityType = None) -> List:
"""获取当前活跃的活动"""
active_list = []
for activity_id in self.active_activities:
activity = self.activities[activity_id]
if activity_type is None or activity.activity_type == activity_type:
active_list.append(activity)
return active_list
def apply_activity_effects(
self,
activity_type: ActivityType,
player: Player,
base_value: int,
context: Dict = None,
) -> int:
"""应用活动效果"""
if context is None:
context = {}
context["base_value"] = base_value
result = base_value
# 获取同类型的所有活跃活动
active_activities = self.get_active_activities(activity_type)
for activity in active_activities:
print(f"为玩家 {player.player_id} 应用活动: {activity.name}")
for effect in activity.effects:
handler = self.effect_handlers.get(effect.type)
if handler:
try:
effect_result = handler.execute(effect.params, player, context)
if effect_result is not None:
result = effect_result
context["base_value"] = result # 更新基础值供后续效果使用
except Exception as e:
print(f"效果执行失败: {effect.type}, 错误: {e}")
return result
class Activity:
"""活动类"""
def __init__(self, data: Dict):
self.id = data["id"]
self.name = data["name"]
self.activity_type = ActivityType(data["activity_type"])
self.start_time = datetime.strptime(data["start_time"], "%Y-%m-%d %H:%M:%S")
self.end_time = datetime.strptime(data["end_time"], "%Y-%m-%d %H:%M:%S")
self.effects = [Effect(effect_data) for effect_data in data["effects"]]
class Effect:
"""效果类"""
def __init__(self, data: Dict):
self.type = EffectType(data["type"])
self.params = data.get("params", {})
+95
View File
@@ -0,0 +1,95 @@
from enum import Enum
from typing import Any
# 活动类型枚举
class ActivityType(Enum):
PLANTING = "planting" # 种植活动
HARVESTING = "harvesting" # 收获活动
FISHING = "fishing" # 钓鱼活动
COMBAT = "combat" # 战斗活动
# 效果类型枚举
class EffectType(Enum):
MULTIPLIER = "multiplier" # 倍数加成
FIXED_BONUS = "fixed_bonus" # 固定加成
BUFF_APPLICATION = "buff_application" # 施加BUFF
QUEST_TRIGGER = "quest_trigger" # 任务触发
class EffectHandler:
"""
效果处理器基类
"""
def execute(self, params: dict, uid: str, context: dict) -> Any:
raise NotImplementedError
class MultiplierHandler(EffectHandler):
"""
倍数效果处理器
"""
def execute(self, params: dict, uid: str, context: dict) -> float:
base_value = context.get("base_value", 0)
multiplier = params.get("value", 1.0)
# 检查条件
if self._check_conditions(params.get("conditions", {}), player):
result = base_value * multiplier
print(f"倍数效果: {base_value} × {multiplier} = {result}")
return result
return base_value
def _check_conditions(self, conditions: dict, player: Player) -> bool:
"""检查生效条件"""
# 等级要求
min_level = conditions.get("min_level", 0)
if player.level < min_level:
return False
# 需要特定物品
required_items = conditions.get("required_items", [])
for item in required_items:
if player.inventory.get(item, 0) <= 0:
return False
return True
class FixedBonusHandler(EffectHandler):
"""固定加成处理器"""
def execute(self, params: dict, player: Player, context: dict) -> int:
base_value = context.get("base_value", 0)
bonus = params.get("value", 0)
result = base_value + bonus
print(f"固定加成: {base_value} + {bonus} = {result}")
return result
class BuffApplicationHandler(EffectHandler):
"""BUFF应用处理器"""
def execute(self, params: dict, player: Player, context: dict) -> None:
buff_id = params["buff_id"]
duration = params.get("duration", 3600) # 默认1小时
properties = params.get("properties", {})
player.add_buff(buff_id, duration, properties)
# 立即应用BUFF效果(如果有)
if "immediate_effect" in params:
self._apply_immediate_effect(params["immediate_effect"], player)
class QuestTriggerHandler(EffectHandler):
"""任务触发处理器"""
def execute(self, params: dict, player: Player, context: dict) -> None:
quest_id = params["quest_id"]
print(f"为玩家 {player.player_id} 触发任务: {quest_id}")
# 这里会调用任务系统来分配任务
+399
View File
@@ -0,0 +1,399 @@
from contextlib import asynccontextmanager
import os
from pathlib import Path
import re
from typing import Any
import aiosqlite
from zhenxun.services.log import logger
from ...utils.config import g_sDBFilePath, g_sDBPath
class CSqlManager:
def __init__(self):
dbPath = Path(g_sDBPath)
if dbPath and not dbPath.exists():
os.makedirs(dbPath, exist_ok=True)
@classmethod
async def cleanup(cls):
if hasattr(cls, "m_pDB") and cls.m_pDB:
await cls.m_pDB.close()
@classmethod
async def init(cls) -> bool:
try:
cls.m_pDB = await aiosqlite.connect(g_sDBFilePath)
cls.m_pDB.row_factory = aiosqlite.Row
return True
except Exception as e:
logger.debug("真寻农场初始化总数据库失败", e=e)
return False
@classmethod
@asynccontextmanager
async def _transaction(cls):
await cls.m_pDB.execute("BEGIN;")
try:
yield
except:
await cls.m_pDB.execute("ROLLBACK;")
raise
else:
await cls.m_pDB.execute("COMMIT;")
@classmethod
async def getTableInfo(cls, tableName: str) -> list:
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", tableName):
raise ValueError(f"Illegal table name: {tableName}")
try:
cursor = await cls.m_pDB.execute(f'PRAGMA table_info("{tableName}")')
rows = await cursor.fetchall()
return [{"name": row[1], "type": row[2]} for row in rows]
except aiosqlite.Error:
return []
@classmethod
async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool:
"""由AI生成
创建表或为已存在表添加缺失字段。
返回 True 表示有变更(创建或新增列),False 则无操作
Args:
tableName (_type_): 表名
columns (_type_): 字典
Returns:
_type_: _description_
"""
info = await cls.getTableInfo(tableName)
existing = {col["name"]: col["type"].upper() for col in info}
desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"}
primaryKey = columns.get("PRIMARY KEY", "")
if not existing:
colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
if primaryKey:
colsDef += f", PRIMARY KEY {primaryKey}"
await cls.m_pDB.execute(f'CREATE TABLE "{tableName}" ({colsDef});')
return True
toAdd = [k for k in desired if k not in existing]
toRemove = [k for k in existing if k not in desired]
typeMismatch = [
k for k in desired if k in existing and existing[k] != desired[k]
]
if toAdd and not toRemove and not typeMismatch:
for col in toAdd:
await cls.m_pDB.execute(
f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}'
)
return True
async with cls._transaction():
tmpTable = f"{tableName}_new"
colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
if primaryKey:
colsDef += f", PRIMARY KEY {primaryKey}"
await cls.m_pDB.execute(f'CREATE TABLE "{tmpTable}" ({colsDef});')
commonCols = [k for k in desired if k in existing]
if commonCols:
colsStr = ", ".join(f'"{c}"' for c in commonCols)
sql = (
f'INSERT INTO "{tmpTable}" ({colsStr}) '
f"SELECT {colsStr} "
f'FROM "{tableName}";'
)
await cls.m_pDB.execute(sql)
await cls.m_pDB.execute(f'DROP TABLE "{tableName}";')
await cls.m_pDB.execute(
f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";'
)
return True
@classmethod
async def executeDB(cls, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if not command:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(command)
return True
except Exception:
return False
@classmethod
async def insert(cls, tableName: str, data: dict) -> bool:
"""
插入数据
Args:
tableName: 表名
data: 要插入的数据字典,键为字段名,值为字段值
Returns:
bool: 是否执行成功
"""
if not data:
return False
try:
# 构建参数化查询
columns = ", ".join(f'"{k}"' for k in data.keys())
placeholders = ", ".join("?" for _ in data.keys())
values = list(data.values())
sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})'
async with cls._transaction():
await cls.m_pDB.execute(sql, values)
return True
except Exception as e:
logger.debug("真寻农场插入数据失败!", e=e)
return False
@classmethod
async def batch_insert(cls, tableName: str, data_list: list) -> bool:
"""
批量插入数据
Args:
tableName: 表名
data_list: 要插入的数据字典列表
Returns:
bool: 是否执行成功
"""
if not data_list:
return False
try:
# 使用第一个字典的键作为所有记录的字段
columns = ", ".join(f'"{k}"' for k in data_list[0].keys())
placeholders = ", ".join("?" for _ in data_list[0].keys())
sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})'
async with cls._transaction():
await cls.m_pDB.executemany(
sql, [list(data.values()) for data in data_list]
)
return True
except Exception as e:
logger.debug("真寻农场批量插入数据失败!", e=e)
return False
@classmethod
async def select(
cls,
tableName: str,
columns: list[Any] | None = None,
where: dict[str, Any] | None = None,
order_by: str | None = None,
limit: int | None = None,
) -> list[dict]:
"""
查询数据
Args:
tableName: 表名
columns: 要查询的字段列表,None表示所有字段
where: 查询条件字典
order_by: 排序字段
limit: 限制返回记录数
Returns:
list: 查询结果列表,每个元素是一个字典
"""
try:
# 构建SELECT部分
if columns:
select_clause = ", ".join(f'"{col}"' for col in columns)
else:
select_clause = "*"
sql = f'SELECT {select_clause} FROM "{tableName}"'
# 构建WHERE部分
params = []
if where:
where_conditions = []
for key, value in where.items():
if isinstance(value, (list, tuple)):
# 处理IN查询
placeholders = ", ".join("?" for _ in value)
where_conditions.append(f'"{key}" IN ({placeholders})')
params.extend(value)
else:
where_conditions.append(f'"{key}" = ?')
params.append(value)
if where_conditions:
sql += " WHERE " + " AND ".join(where_conditions)
# 构建ORDER BY部分
if order_by:
sql += f" ORDER BY {order_by}"
# 构建LIMIT部分
if limit:
sql += f" LIMIT {limit}"
cursor = await cls.m_pDB.execute(sql, params)
rows = await cursor.fetchall()
# 转换为字典列表
result = []
for row in rows:
result.append(dict(row))
return result
except Exception as e:
logger.debug("真寻农场查询数据失败!", e=e)
return []
@classmethod
async def update(cls, tableName: str, data: dict, where: dict) -> bool:
"""
更新数据
Args:
tableName: 表名
data: 要更新的数据字典
where: 更新条件字典
Returns:
bool: 是否执行成功
"""
if not data:
return False
if not where:
return False
try:
# 构建SET部分
set_conditions = []
params = []
for key, value in data.items():
set_conditions.append(f'"{key}" = ?')
params.append(value)
# 构建WHERE部分
where_conditions = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql = f'UPDATE "{tableName}" SET {", ".join(set_conditions)} WHERE {" AND ".join(where_conditions)}'
async with cls._transaction():
cursor = await cls.m_pDB.execute(sql, params)
# 检查是否影响了行
return cursor.rowcount > 0
except Exception as e:
logger.debug("真寻农场更新数据失败!", e=e)
return False
@classmethod
async def delete(cls, tableName: str, where: dict) -> bool:
"""
删除数据
Args:
tableName: 表名
where: 删除条件字典
Returns:
bool: 是否执行成功
"""
if not where:
return False
try:
# 构建WHERE部分
where_conditions = []
params = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql = f'DELETE FROM "{tableName}" WHERE {" AND ".join(where_conditions)}'
async with cls._transaction():
cursor = await cls.m_pDB.execute(sql, params)
# 检查是否影响了行
return cursor.rowcount > 0
except Exception as e:
logger.debug("真寻农场删除数据失败!", e=e)
return False
@classmethod
async def exists(cls, tableName: str, where: dict) -> bool:
"""
检查记录是否存在
Args:
tableName: 表名
where: 查询条件字典
Returns:
bool: 是否存在符合条件的记录
"""
try:
result = await cls.select(tableName, columns=["1"], where=where, limit=1)
return len(result) > 0
except Exception as e:
logger.debug("真寻农场检查数据失败!", e=e)
return False
@classmethod
async def count(cls, tableName: str, where: dict = {}) -> int:
"""
统计记录数量
Args:
tableName: 表名
where: 查询条件字典
Returns:
int: 记录数量
"""
try:
# 构建WHERE部分
sql = f'SELECT COUNT(*) as count FROM "{tableName}"'
params = []
if where:
where_conditions = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql += " WHERE " + " AND ".join(where_conditions)
cursor = await cls.m_pDB.execute(sql, params)
row = await cursor.fetchone()
return row["count"] if row else 0
except Exception as e:
logger.debug("真寻农场统计数据失败!", e=e)
return 0
g_pSqlManager = CSqlManager()
+281
View File
@@ -0,0 +1,281 @@
from contextlib import asynccontextmanager
import os
import aiosqlite
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from ...utils.config import g_bIsDebug, g_sPlantPath, g_sResourcePath
from ...utils.request import g_pRequestManager
class CPlantManager:
def __init__(self):
try:
os.mkdir(g_sPlantPath)
except FileExistsError:
pass
async def cleanup(self):
if hasattr(self, "m_pDB") and self.m_pDB:
await self.m_pDB.close()
async def init(self) -> bool:
try:
_ = os.path.exists(g_sPlantPath)
if g_bIsDebug:
self.m_pDB = await aiosqlite.connect(
str(g_sPlantPath.parent / "plant-test.db")
)
else:
self.m_pDB = await aiosqlite.connect(str(g_sPlantPath))
self.m_pDB.row_factory = aiosqlite.Row
return True
except Exception as e:
logger.warning("初始化植物数据库失败", e=e)
return False
@asynccontextmanager
async def _transaction(self):
await self.m_pDB.execute("BEGIN;")
try:
yield
except:
await self.m_pDB.execute("ROLLBACK;")
raise
else:
await self.m_pDB.execute("COMMIT;")
async def executeDB(self, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if not command:
logger.warning("数据库语句长度为空!")
return False
try:
async with self._transaction():
await self.m_pDB.execute(command)
return True
except Exception as e:
logger.warning(f"数据库语句执行出错: {command}", e=e)
return False
async def getPlantByName(self, name: str) -> dict | None:
"""根据作物名称查询记录
Args:
name (str): 作物名称
Returns:
dict | None: 返回记录字典,未找到返回None
"""
try:
async with self.m_pDB.execute(
"SELECT * FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
except Exception as e:
logger.warning(f"查询作物失败: {name}", e=e)
return None
async def getPlantPhaseByName(self, name: str) -> list[int]:
"""根据作物名称获取作物各个阶段
Args:
name (str): 作物名称
Returns:
list: 阶段数组
"""
try:
async with self.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return []
phase = row[0].split(",")
seen = set()
result = []
for x in phase:
num = int(x)
if num not in seen:
seen.add(num)
result.append(num)
return result
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return []
async def getPlantPhaseNumberByName(self, name: str) -> int:
"""根据作物名称获取作物总阶段数
Args:
name (str): 作物名称
Returns:
int: 总阶段数
"""
try:
async with self.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return -1
phase = row[0].split(",")
# 去重
seen = set()
result = []
for x in phase:
if x not in seen:
seen.add(x)
result.append(x)
return len(result)
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return -1
async def getPlantAgainByName(self, name: str) -> int:
"""根据作物名称获取作物再次成熟时间
Args:
name (str): 作物名称
Returns:
int: 再次成熟时间 单位:h
"""
try:
async with self.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return -1
phase = row[0].split(",")
again = phase[-1] - phase[3] / 60 / 60
return again
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return -1
async def existsPlant(self, name: str) -> bool:
"""判断作物是否存在
Args:
name (str): 作物名称
Returns:
bool: 存在返回True,否则False
"""
try:
async with self.m_pDB.execute(
"SELECT 1 FROM plant WHERE name = ? LIMIT 1", (name,)
) as cursor:
row = await cursor.fetchone()
return True if row else False
except Exception as e:
logger.warning(f"检查作物存在性失败: {name}", e=e)
return False
async def countPlants(self, onlyBuy: bool = False) -> int:
"""获取作物总数
Args:
onlyBuy (bool): 若为True,仅统计isBuy=1的记录,默认False
Returns:
int: 符合条件的记录数
"""
try:
if onlyBuy:
sql = "SELECT COUNT(*) FROM plant WHERE isBuy = 1"
params: tuple = ()
else:
sql = "SELECT COUNT(*) FROM plant"
params: tuple = ()
async with self.m_pDB.execute(sql, params) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.warning(f"统计作物数量失败, onlyBuy={onlyBuy}", e=e)
return 0
async def listPlants(self) -> list[dict]:
"""查询所有作物记录"""
try:
async with self.m_pDB.execute(
"SELECT * FROM plant ORDER BY level"
) as cursor:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
except Exception as e:
logger.warning("查询所有作物失败", e=e)
return []
async def downloadPlant(self) -> bool:
"""遍历所有作物,下载各阶段图片及icon文件到指定文件夹
Returns:
bool: 全部下载完成返回True,如有失败返回False
"""
success = True
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
baseUrl = baseUrl.rstrip("/") + ":8998/file"
try:
plants = await self.listPlants()
for plant in plants:
name = plant["name"]
phaseCount = await self.getPlantPhaseNumberByName(name)
saveDir = os.path.join(g_sResourcePath, "plant", name)
begin = 0 if plant["general"] == 0 else 1
for idx in range(begin, phaseCount + 1):
fileName = f"{idx}.png"
fullPath = os.path.join(saveDir, fileName)
if os.path.exists(fullPath):
continue
url = f"{baseUrl}/{name}/{idx}.png"
if not await g_pRequestManager.download(url, saveDir, f"{idx}.png"):
success = False
iconName = "icon.png"
iconPath = os.path.join(saveDir, iconName)
if not os.path.exists(iconPath):
iconUrl = f"{baseUrl}/{name}/{iconName}"
if not await g_pRequestManager.download(iconUrl, saveDir, iconName):
success = False
return success
except Exception as e:
logger.warning(f"下载作物资源异常: {e}")
return False
+211
View File
@@ -0,0 +1,211 @@
import math
from ...utils.tool import g_pToolManager
from .database import CSqlManager
class CUserDB(CSqlManager):
def __init__(self):
self.currencies: list[str] = ["point", "vipPoint"]
async def initDB(self):
userInfo = {
"uid": "TEXT PRIMARY KEY", # 用户Uid
"name": "TEXT NOT NULL", # 农场名称
"exp": "INTEGER DEFAULT 0", # 经验值
"point": "INTEGER DEFAULT 0", # 金币
"vipPoint": "INTEGER DEFAULT 0", # 点券
"soil": "INTEGER DEFAULT 3", # 解锁土地数量
"stealTime": "TEXT DEFAULT ''", # 偷菜时间字符串
"stealCount": "INTEGER DEFAULT 0", # 剩余偷菜次数
}
await self.ensureTableSchema("user", userInfo)
async def initUserInfo(self, uid: str, name: str) -> bool:
"""初始化用户信息
Args:
uid (str): 用户ID
name (str): 农场名称
Returns:
bool: 是否成功初始化用户信息
"""
nowStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
result = await self.insert(
"user",
data={
"uid": uid,
"name": name,
"exp": 0,
"point": 500,
"soil": 3,
"stealTime": nowStr,
"stealCount": 5,
},
)
return result
async def getUserInfoByUid(self, uid: str) -> dict:
"""根据用户ID获取用户信息
Args:
uid (str): 用户ID
Returns:
dict: 用户信息字典,未找到返回空字典
"""
if uid == "":
return {}
records = await self.select("user", where={"uid": uid})
return records[0] if records else {}
async def isRegistered(self, uid: str) -> bool:
"""检查用户是否注册农场
Args:
uid (str): 用户ID
Returns:
bool: 是否注册农场
"""
if uid == "":
return False
return await self.exists("user", where={"uid": uid})
async def updatePoint(self, uid: str, type: str, index: int) -> bool:
"""更新货币
Args:
uid (str): 用户ID
type (str): 货币类型 point/vipPoint
index (int): 更新后的数量
Returns:
bool: 是否成功更新货币
"""
if type not in self.currencies:
return False
if index < 0:
index = 0
await self.update("user", {type: index}, {"uid": uid})
return True
async def updateExp(self, uid: str, exp: int) -> bool:
"""更新经验值
Args:
uid (str): 用户ID
exp (int): 更新后的经验值
Returns:
bool: 是否成功更新经验值
"""
if exp < 0:
exp = 0
return await self.update("user", {"exp": exp}, {"uid": uid})
async def updateName(self, uid: str, name: str) -> str:
"""更新农场名称
Args:
uid (str): 用户ID
name (str): 农场名称
Returns:
bool: 是否成功更新农场名称
"""
safeName = g_pToolManager.sanitize_username(name)
if safeName == "神秘农夫":
return "error"
if await self.update("user", {"name": safeName}, {"uid": uid}):
return "success"
return "error1"
async def getUserLevelByUid(self, uid: str) -> tuple[int, int, int]:
"""获取用户等级信息
Args:
uid (str): 用户Uid
Returns:
tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验)
失败返回(-1, -1, -1)
"""
if not uid:
return -1, -1, -1
records = await self.select("user", where={"uid": uid}, columns=["exp"])
if not records:
return -1, -1, -1
try:
exp = int(records[0].get("exp", 0))
except Exception:
exp = 0
levelStep = 200 # 每级经验增量
discriminant = 1 + 8 * exp / levelStep
level = int((-1 + math.sqrt(discriminant)) // 2)
if level < 0:
level = 0
def cumExp(k: int) -> int:
return levelStep * k * (k + 1) // 2
totalExpCurrentLevel = cumExp(level)
totalExpNextLevel = cumExp(level + 1)
currentExp = exp - totalExpCurrentLevel
return level, totalExpNextLevel, currentExp
async def updateStealCountByUid(
self, uid: str, stealTime: str, stealCount: int
) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
stealTime (str): 偷菜日期
stealCount (int): 新剩余偷菜次数
Returns:
bool: 是否更新成功
"""
if not uid or stealCount < 0:
return False
return await self.update(
"user", {"stealTime": stealTime, "stealCount": stealCount}, {"uid": uid}
)
async def updateFieldByUid(self, uid: str, field: str, value) -> bool:
"""更新单字段信息
Args:
uid (str): 用户Uid
field (str): 字段名称
Returns:
bool: 是否成功更新字段
"""
if not uid or not field:
return False
return await self.update("user", {field: value}, {"uid": uid})
+158
View File
@@ -0,0 +1,158 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserItemDB(CSqlManager):
@classmethod
async def initDB(cls):
userItem = {
"uid": "TEXT NOT NULL", # 用户Uid
"item": "TEXT NOT NULL", # 物品名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, item)",
}
await cls.ensureTableSchema("userItem", userItem)
@classmethod
async def getUserItemByName(cls, uid: str, item: str) -> int | None:
"""根据道具名称查询某一项数量
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
Optional[int]: 数量(不存在返回None)
"""
if not uid or not item:
return None
try:
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?", (uid, item)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning("getUserItemByName查询失败!", e=e)
return None
@classmethod
async def getUserItemByUid(cls, uid: str) -> dict:
"""根据用户Uid获取全部道具信息
Args:
uid (str): 用户uid
Returns:
dict: {itemName: count, ...}
"""
if not uid:
return {}
try:
cursor = await cls.m_pDB.execute(
"SELECT item, count FROM userItem WHERE uid = ?", (uid,)
)
rows = await cursor.fetchall()
return {row["item"]: row["count"] for row in rows}
except Exception as e:
logger.warning("getUserItemByUid查询失败!", e=e)
return {}
@classmethod
async def deleteUserItemByName(cls, uid: str, item: str) -> bool:
"""根据道具名删除道具
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
bool: 是否删除成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?", (uid, item)
)
return True
except Exception as e:
logger.warning("deleteUserItemByName失败!", e=e)
return False
@classmethod
async def updateUserItemByName(cls, uid: str, item: str, count: int) -> bool:
"""根据道具名直接更新道具数量
Args:
uid (str): 用户uid
item (str): 道具名称
count (int): 要更新的新数量
Returns:
bool: 是否更新成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
if count <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?", (uid, item)
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(count, uid, item),
)
return True
except Exception as e:
logger.warning("updateUserItemByName失败!", e=e)
return False
@classmethod
async def addUserItemByUid(cls, uid: str, item: str, count: int = 1) -> bool:
"""根据用户uid添加道具信息
Args:
uid (str): 用户uid
item (str): 道具名称
count (int, optional): 数量.Defaults to 1.
Returns:
bool: 是否添加成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?", (uid, item)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item),
)
else:
await cls.m_pDB.execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(newCount, uid, item),
)
else:
if count > 0:
await cls.m_pDB.execute(
"INSERT INTO userItem (uid, item, count) VALUES (?, ?, ?)",
(uid, item, count),
)
return True
except Exception as e:
logger.warning("addUserItemByUid失败!", e=e)
return False
+205
View File
@@ -0,0 +1,205 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserPlantDB(CSqlManager):
@classmethod
async def initDB(cls):
userPlant = {
"uid": "TEXT NOT NULL", # 用户Uid
"plant": "TEXT NOT NULL", # 作物名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"isLock": "INTEGER NOT NULL DEFAULT 0", # 是否上锁 0=没有,非0=有
"PRIMARY KEY": "(uid, plant)",
}
await cls.ensureTableSchema("userPlant", userPlant)
@classmethod
async def addUserPlantByUid(cls, uid: str, plant: str, count: int = 1) -> bool:
"""根据用户uid添加作物信息
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
# 检查是否已存在该作物
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant),
) as cursor:
row = await cursor.fetchone()
if row:
# 如果作物已存在,则更新数量
new_count = row[0] + count
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(new_count, uid, plant),
)
else:
# 如果作物不存在,则插入新记录
await cls.m_pDB.execute(
"INSERT INTO userPlant (uid, plant, count) VALUES (?, ?, ?)",
(uid, plant, count),
)
return True
except Exception as e:
logger.warning("addUserPlantByUid 失败!", e=e)
return False
@classmethod
async def getUserPlantByUid(cls, uid: str) -> dict[str, int]:
"""根据用户uid获取全部作物信息
Args:
uid (str): 用户uid
Returns:
Dict[str, int]: 作物名称和数量
"""
cursor = await cls.m_pDB.execute(
"SELECT plant, count FROM userPlant WHERE uid=?", (uid,)
)
rows = await cursor.fetchall()
return {row["plant"]: row["count"] for row in rows}
@classmethod
async def getUserPlantByName(cls, uid: str, plant: str) -> int | None:
"""根据作物名称获取用户的作物数量
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
Optional[int]: 作物数量
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning("getUserPlantByName 查询失败!", e=e)
return None
@classmethod
async def checkUserPlantByName(cls, uid: str, plant: str) -> bool:
"""根据作物名称判断用户作物仓库是否存在该作物
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否存在
"""
try:
async with cls.m_pDB.execute(
"SELECT * FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return True if row else False
except Exception as e:
logger.warning("checkUserPlantByName 查询失败!", e=e)
return False
@classmethod
async def updateUserPlantByName(cls, uid: str, plant: str, count: int) -> bool:
"""更新 userPlant 表中某个作物的数量
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 新的作物数量
Returns:
bool: 是否更新成功
"""
try:
if count <= 0:
return await cls.deleteUserPlantByName(uid, plant)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(count, uid, plant),
)
return True
except Exception as e:
logger.warning("updateUserPlantByName 失败!", e=e)
return False
@classmethod
async def lockUserPlantByName(cls, uid: str, plant: str, lock: int) -> bool:
"""给作物加锁,防止一键售卖
Args:
uid (str): 用户uid
plant (str): 作物名称
lock (int): 0为解锁,非0均为加锁
Returns:
bool: 是否加锁成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userPlant SET isLock = ? WHERE uid = ? AND plant = ?",
(lock, uid, plant),
)
return True
except Exception as e:
logger.warning("lockUserPlantByName 失败!", e=e)
return False
@classmethod
async def checkPlantLockByName(cls, uid: str, plant: str) -> bool:
"""根据作物名称判断是否加锁
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否加锁
"""
try:
async with cls.m_pDB.execute(
"SELECT isLock FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] > 0 if row else False
except Exception as e:
logger.warning("checkUserPlantByName 查询失败!", e=e)
return False
@classmethod
async def deleteUserPlantByName(cls, uid: str, plant: str) -> bool:
"""从 userPlant 表中删除某个作物记录
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否删除成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
)
return True
except Exception as e:
logger.warning("deleteUserPlantByName 失败!", e=e)
return False
+212
View File
@@ -0,0 +1,212 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserSeedDB(CSqlManager):
@classmethod
async def initDB(cls):
userSeed = {
"uid": "TEXT NOT NULL", # 用户Uid
"seed": "TEXT NOT NULL", # 种子名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, seed)",
}
await cls.ensureTableSchema("userSeed", userSeed)
@classmethod
async def addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息(事务版本)
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(newCount, uid, seed),
)
else:
newCount = count
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, count),
)
if newCount <= 0:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning("addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def _addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息(非事务版,复用其他非事务接口)"""
try:
existing = await cls.getUserSeedByName(uid, seed)
newCount = (existing or 0) + count
if existing is not None:
await cls._updateUserSeedByName(uid, seed, newCount)
else:
await cls.m_pDB.execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, newCount),
)
if newCount <= 0:
await cls._deleteUserSeedByName(uid, seed)
return True
except Exception as e:
logger.warning("_addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def getUserSeedByName(cls, uid: str, seed: str) -> int | None:
"""根据种子名称获取种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
Optional[int]: 种子数量
"""
try:
async with cls.m_pDB.execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning("getUserSeedByName 查询失败!", e=e)
return None
@classmethod
async def getUserSeedByUid(cls, uid: str) -> dict:
"""根据用户Uid获取仓库全部种子信息
Args:
uid (str): 用户uid
Returns:
dict: 种子信息
"""
cursor = await cls.m_pDB.execute(
"SELECT seed, count FROM userSeed WHERE uid=?", (uid,)
)
rows = await cursor.fetchall()
return {row["seed"]: row["count"] for row in rows}
@classmethod
async def updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed),
)
return True
except Exception as e:
logger.warning("updateUserSeedByName失败!", e=e)
return False
@classmethod
async def _updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.m_pDB.execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed),
)
return True
except Exception as e:
logger.warning("updateUserSeedByName失败!", e=e)
return False
@classmethod
async def deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning("deleteUserSeedByName 删除失败!", e=e)
return False
@classmethod
async def _deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
await cls.m_pDB.execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning("deleteUserSeedByName 删除失败!", e=e)
return False
+288
View File
@@ -0,0 +1,288 @@
import calendar
from datetime import timedelta
import random
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from ...utils.config import g_bIsDebug
from ...utils.json import g_pJsonManager
from ...utils.tool import g_pToolManager
from ..dbService import g_pDBService
from .database import CSqlManager
class CUserSignDB(CSqlManager):
@classmethod
async def initDB(cls):
# userSignLog 表结构,每条为一次签到事件
userSignLog = {
"uid": "TEXT NOT NULL", # 用户ID
"signDate": "DATE NOT NULL", # 签到日期
"isSupplement": "TINYINT NOT NULL DEFAULT 0", # 是否补签
"exp": "INT NOT NULL DEFAULT 0", # 当天签到经验
"point": "INT NOT NULL DEFAULT 0", # 当天签到金币
"createdAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))", # 创建时间 # noqa: E501
"PRIMARY KEY": "(uid, signDate)",
}
# userSignSummary 表结构,每用户一行用于缓存签到状态
userSignSummary = {
"uid": "TEXT PRIMARY KEY NOT NULL", # 用户ID
"totalSignDays": "INT NOT NULL DEFAULT 0", # 累计签到天数
"currentMonth": "CHAR(7) NOT NULL DEFAULT ''", # 当前月份(如2025-05)
"monthSignDays": "INT NOT NULL DEFAULT 0", # 本月签到次数
"lastSignDate": "DATE DEFAULT NULL", # 上次签到日期
"continuousDays": "INT NOT NULL DEFAULT 0", # 连续签到天数
"supplementCount": "INT NOT NULL DEFAULT 0", # 补签次数
"updatedAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))", # 更新时间 # noqa: E501
}
await cls.ensureTableSchema("userSignLog", userSignLog)
await cls.ensureTableSchema("userSignSummary", userSignSummary)
@classmethod
async def getUserSignRewardByDate(cls, uid: str, date: str) -> tuple[int, int]:
"""根据指定日期获取用户签到随机奖励
Args:
uid (str): 用户Uid
date (str): 用户签到日期 示例:2025-05-27
Returns:
tuple[int, int]: 经验、金币
"""
try:
async with cls._transaction():
async with cls.m_pDB.execute(
"SELECT exp, point FROM userSignLog WHERE uid=? AND signDate=?",
(uid, date),
) as cursor:
row = await cursor.fetchone()
if row is None:
return 0, 0
exp = row["exp"]
point = row["point"]
return exp, point
except Exception as e:
logger.warning("获取用户签到数据失败", e=e)
return 0, 0
@classmethod
async def getUserSignCountByDate(cls, uid: str, monthStr: str) -> int:
"""根据日期查询用户签到总天数
Args:
uid (str): 用户Uid
monthStr (str): 需要查询的日期 示例: 2025-05
Returns:
int: 查询月总签到天数
"""
try:
sql = "SELECT COUNT(*) FROM userSignLog WHERE uid=? AND signDate LIKE ?"
param = f"{monthStr}-%"
async with cls.m_pDB.execute(sql, (uid, param)) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.warning("统计用户月签到次数失败", e=e)
return 0
@classmethod
async def hasSigned(cls, uid: str, signDate: str) -> bool:
"""判断指定日期是否已签到
Args:
uid (int): 用户ID
signDate (str): 日期字符串 'YYYY-MM-DD'
Returns:
bool: True=已签到,False=未签到
"""
try:
sql = "SELECT 1 FROM userSignLog WHERE uid=? AND signDate=? LIMIT 1"
async with cls.m_pDB.execute(sql, (uid, signDate)) as cursor:
row = await cursor.fetchone()
return row is not None
except Exception as e:
logger.warning("查询是否已签到失败", e=e)
return False
@classmethod
async def sign(cls, uid: str, signDate: str = "") -> int:
"""签到
Args:
uid (int): 用户ID
signDate (str): 日期字符串 'YYYY-MM-DD' 不传默认当前系统日期
Returns:
bool: 0: 签到失败 1: 签到成功 2: 重复签到
"""
try:
player = await g_pToolManager.getPlayerByUid(uid)
if not player:
return 0
if not signDate:
signDate = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
if await cls.hasSigned(uid, signDate):
return 2
todayStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
isSupplement = 0 if signDate == todayStr else 1
expMax, expMin, pointMax, pointMin = [
g_pJsonManager.m_pSign.get(key, default)
for key, default in (
("exp_max", 50),
("exp_min", 5),
("point_max", 2000),
("point_min", 200),
)
]
exp = random.randint(expMin, expMax)
point = random.randint(pointMin, pointMax)
vipPoint = 0
async with cls._transaction():
await cls.m_pDB.execute(
"INSERT INTO userSignLog (uid, signDate, isSupplement, exp, point) VALUES (?, ?, ?, ?, ?)",
(uid, signDate, isSupplement, exp, point),
)
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSignSummary WHERE uid=?", (uid,)
)
row = await cursor.fetchone()
currentMonth = signDate[:7]
if row:
monthSignDays = (
row["monthSignDays"] + 1
if row["currentMonth"] == currentMonth
else 1
)
lastDate = row["lastSignDate"]
prevDate = (
g_pToolManager.dateTime().strptime(signDate, "%Y-%m-%d")
- timedelta(days=1)
).strftime("%Y-%m-%d")
continuousDays = (
row["continuousDays"] + 1 if lastDate == prevDate else 1
)
supplementCount = (
row["supplementCount"] + 1
if isSupplement
else row["supplementCount"]
)
await cls.m_pDB.execute(
"""
UPDATE userSignSummary
SET totalSignDays=totalSignDays+1,
currentMonth=?,
monthSignDays=?,
lastSignDate=?,
continuousDays=?,
supplementCount=?
WHERE uid=?
""",
(
currentMonth,
monthSignDays,
signDate,
continuousDays,
supplementCount,
uid,
),
)
else:
monthSignDays = 1
await cls.m_pDB.execute(
"""
INSERT INTO userSignSummary
(uid, totalSignDays, currentMonth, monthSignDays, lastSignDate, continuousDays, supplementCount)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
uid,
1,
currentMonth,
monthSignDays,
signDate,
1,
1 if isSupplement else 0,
),
)
# 计算累签奖励
reward = g_pJsonManager.m_pSign["continuou"].get(f"{monthSignDays}", None)
if reward:
point += reward.get("point", 0)
exp += reward.get("exp", 0)
vipPoint = reward.get("vipPoint", 0)
plant = reward.get("plant", {})
if plant:
for key, value in plant.items():
await g_pDBService.userSeed.addUserSeedByUid(uid, key, value)
if g_bIsDebug:
exp += 9999
# 向数据库更新
await player.addExp(exp)
await player.addPoint("point", point)
if vipPoint > 0:
await player.addPoint("vipPoint", vipPoint)
return 1
except Exception as e:
logger.warning("执行签到失败", e=e)
return 0
@classmethod
async def drawSignCalendarImage(cls, uid: str, year: int, month: int):
# 绘制签到图,自动提取数据库中该用户该月的签到天数
cellSize = 80
padding = 40
titleHeight = 80
cols = 7
rows = 6
width = cellSize * cols + padding * 2
height = cellSize * rows + padding * 2 + titleHeight
img = BuildImage(width, height, color=(255, 255, 255))
await img.text((padding, 20), f"{year}年{month}月签到表", font_size=36)
firstWeekday, totalDays = calendar.monthrange(year, month)
monthStr = f"{year:04d}-{month:02d}"
try:
sql = "SELECT signDate FROM userSignLog WHERE uid=? AND signDate LIKE ?"
async with cls.m_pDB.execute(sql, (uid, f"{monthStr}-%")) as cursor:
rows = await cursor.fetchall()
signedDays = {int(r[0][-2:]) for r in rows if r[0][-2:].isdigit()}
except Exception as e:
logger.warning("绘制签到图时数据库查询失败", e=e)
signedDays = set()
for day in range(1, totalDays + 1):
index = day + firstWeekday - 1
row = index // cols
col = index % cols
x1 = padding + col * cellSize
y1 = padding + titleHeight + row * cellSize
x2 = x1 + cellSize - 10
y2 = y1 + cellSize - 10
color = (112, 196, 112) if day in signedDays else (220, 220, 220)
await img.rectangle((x1, y1, x2, y2), fill=color, outline="black", width=2)
await img.text((x1 + 10, y1 + 10), str(day), font_size=24)
return img
+541
View File
@@ -0,0 +1,541 @@
import math
from zhenxun.services.log import logger
from ...utils.config import g_bIsDebug
from ...utils.tool import g_pToolManager
from ..dbService import g_pDBService
from .database import CSqlManager
class CUserSoilDB(CSqlManager):
@classmethod
async def initDB(cls):
userSoil = {
"uid": "TEXT NOT NULL",
"soilIndex": "INTEGER NOT NULL", # 地块索引从1开始
"plantName": "TEXT DEFAULT ''", # 作物名称
"plantTime": "INTEGER DEFAULT 0", # 播种时间
"matureTime": "INTEGER DEFAULT 0", # 成熟时间
"soilLevel": "INTEGER DEFAULT 0", # 土地等级 0=普通地,1=红土地,2=黑土地,3=金土地
"wiltStatus": "INTEGER DEFAULT 0", # 枯萎状态 0=未枯萎,1=枯萎
"fertilizerStatus": "INTEGER DEFAULT 0", # 施肥状态 0=未施肥,1=施肥 2=增肥
"bugStatus": "INTEGER DEFAULT 0", # 虫害状态 0=无虫害,1=有虫害
"weedStatus": "INTEGER DEFAULT 0", # 杂草状态 0=无杂草,1=有杂草
"waterStatus": "INTEGER DEFAULT 0", # 缺水状态 0=不缺水,1=缺水
"harvestCount": "INTEGER DEFAULT 0", # 收获次数
"isSoilPlanted": "INTEGER DEFAULT NULL", # 是否种植作物 0=没有,1=有
"PRIMARY KEY": "(uid, soilIndex)",
}
await cls.ensureTableSchema("userSoil", userSoil)
@classmethod
async def nextPhase(cls, uid: str, soilIndex: int):
"""将指定地块的作物进入下个阶段
Args:
soilIndex (int): 地块索引 从1开始
"""
if not g_bIsDebug:
return
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo["plantName"])
if not plantInfo:
return
currentTime = g_pToolManager.dateTime().now().timestamp()
phaseList = await g_pDBService.plant.getPlantPhaseByName(soilInfo["plantName"])
if currentTime >= soilInfo["matureTime"]:
return
elapsedTime = currentTime - soilInfo["plantTime"]
currentStage = currentStage = sum(1 for thr in phaseList if elapsedTime >= thr)
t = int(soilInfo["plantTime"]) - phaseList[currentStage]
s = int(soilInfo["matureTime"]) - phaseList[currentStage]
await cls.updateUserSoilFields(
uid, soilIndex, {"plantTime": t, "matureTime": s}
)
logger.debug(
f"当前阶段{currentStage}, 阶段时间{phaseList[currentStage]}, 播种时间{t}, 收获时间{s}"
)
@classmethod
async def matureNow(cls, uid: str, soilIndex: int):
"""将指定地块的作物直接成熟
Args:
uid (str): 用户ID
soilIndex (int): 地块索引(从1开始)
"""
# 与 nextPhase 不同:无需调试模式检查,允许在任何模式下调用
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return
plantName = soilInfo.get("plantName")
if not plantName:
return
plantInfo = await g_pDBService.plant.getPlantByName(plantName)
if not plantInfo:
return
currentTime = int(g_pToolManager.dateTime().now().timestamp())
# 如果当前时间已经超过或等于成熟时间,则作物已成熟或可收获
if currentTime >= soilInfo["matureTime"]:
return
# 将作物成熟时间直接更新为当前时间,实现立即成熟
await cls.updateUserSoilFields(uid, soilIndex, {"matureTime": currentTime})
@classmethod
async def getUserFarmByUid(cls, uid: str) -> dict:
"""获取指定用户的旧农场数据
Args:
uid (str): 用户ID
Returns:
dict: 包含字段名-值的字典; 若无数据则返回空字典
"""
cursor = await cls.m_pDB.execute("SELECT * FROM soil WHERE uid = ?", (uid,))
row = await cursor.fetchone()
if not row:
return {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def insertUserSoil(cls, soilInfo: dict):
"""插入一条新的 userSoil 记录
Args:
soilInfo (dict): 新土地数据
Returns:
None
"""
async with cls._transaction():
await cls.m_pDB.execute(
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount, isSoilPlanted)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
soilInfo["soilIndex"],
soilInfo.get("plantName", ""),
soilInfo.get("plantTime", 0),
soilInfo.get("matureTime", 0),
soilInfo.get("soilLevel", 0),
soilInfo.get("wiltStatus", 0),
soilInfo.get("fertilizerStatus", 0),
soilInfo.get("bugStatus", 0),
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
soilInfo.get("isSoilPlanted", 0),
),
)
@classmethod
async def _insertUserSoil(cls, soilInfo: dict):
"""插入一条新的 userSoil 记录
Args:
soilInfo (dict): 新土地数据
Returns:
None
"""
await cls.m_pDB.execute(
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount, isSoilPlanted)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
soilInfo["soilIndex"],
soilInfo.get("plantName", ""),
soilInfo.get("plantTime", 0),
soilInfo.get("matureTime", 0),
soilInfo.get("soilLevel", 0),
soilInfo.get("wiltStatus", 0),
soilInfo.get("fertilizerStatus", 0),
soilInfo.get("bugStatus", 0),
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
soilInfo.get("isSoilPlanted", 0),
),
)
@classmethod
async def getUserSoil(cls, uid: str, soilIndex: int) -> dict:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
dict: 记录存在返回字段-值字典,否则返回 None
"""
async with cls._transaction():
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
(uid, soilIndex),
)
row = await cursor.fetchone()
if not row:
return {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def _getUserSoil(cls, uid: str, soilIndex: int) -> dict | None:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
dict | None: 记录存在返回字段-值字典,否则返回 None
"""
cursor = await cls.m_pDB.execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
(uid, soilIndex),
)
row = await cursor.fetchone()
if not row:
return None
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def countSoilByLevel(cls, uid: str, soilLevel: int) -> int:
"""统计指定用户在指定土地等级的土地数量
Args:
uid (str): 用户ID
soilLevel (int): 土地等级
Returns:
int: 符合条件的土地数量
"""
async with cls._transaction():
cursor = await cls.m_pDB.execute(
"SELECT COUNT(*) FROM userSoil WHERE uid = ? AND soilLevel = ?",
(uid, soilLevel),
)
row = await cursor.fetchone()
return row[0] if row else 0
@classmethod
async def updateUserSoil(cls, uid: str, soilIndex: int, field: str, value):
"""更新指定用户土地的单个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
field (str): 需更新的字段名
value: 新值
Returns:
None
"""
async with cls._transaction():
await cls.m_pDB.execute(
f"UPDATE userSoil SET {field} = ? WHERE uid = ? AND soilIndex = ?",
(value, uid, soilIndex),
)
@classmethod
async def _updateUserSoil(cls, uid: str, soilIndex: int, field: str, value):
"""更新指定用户土地的单个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
field (str): 需更新的字段名
value: 新值
Returns:
None
"""
await cls.m_pDB.execute(
f"UPDATE userSoil SET {field} = ? WHERE uid = ? AND soilIndex = ?",
(value, uid, soilIndex),
)
@classmethod
async def updateUserSoilFields(
cls, uid: str, soilIndex: int, updates: dict
) -> bool:
"""批量更新指定用户土地的多个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
updates (dict): 字段-新值的字典
Returns:
bool: 如果无可更新字段则返回 False,否则更新成功返回 True
"""
# 允许更新的列白名单
allowedFields = {
"plantName",
"plantTime",
"matureTime",
"soilLevel",
"wiltStatus",
"fertilizerStatus",
"bugStatus",
"weedStatus",
"waterStatus",
"harvestCount",
"isSoilPlanted",
}
setClauses = []
values = []
for field, value in updates.items():
if field not in allowedFields:
continue
setClauses.append(f'"{field}" = ?')
values.append(value)
if not setClauses:
return False
values.extend([uid, soilIndex])
sql = f"UPDATE userSoil SET {', '.join(setClauses)} WHERE uid = ? AND soilIndex = ?"
try:
async with cls._transaction():
await cls.m_pDB.execute(sql, tuple(values))
return True
except Exception as e:
logger.error(f"批量更新土地字段失败: {e}")
return False
@classmethod
async def deleteUserSoil(cls, uid: str, soilIndex: int):
"""删除指定用户的土地记录
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
None
"""
async with cls._transaction():
await cls.m_pDB.execute(
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def _deleteUserSoil(cls, uid: str, soilIndex: int):
"""删除指定用户的土地记录
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
None
"""
await cls.m_pDB.execute(
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def sowingByPlantName(cls, uid: str, soilIndex: int, plantName: str) -> bool:
"""播种指定作物到用户土地区
Args:
uid (str): 用户ID
soilIndex (int): 土地区索引
plantName (str): 植物名
Returns:
bool: 播种成功返回 True,否则返回 False
"""
# 校验土地区是否已种植
soilInfo = await cls.getUserSoil(uid, soilIndex)
if soilInfo and soilInfo.get("plantName"):
return False
# 获取植物配置
plantCfg = await g_pDBService.plant.getPlantByName(plantName)
if not plantCfg:
logger.error(f"未知植物: {plantName}")
return False
nowTs = int(g_pToolManager.dateTime().now().timestamp())
time = int(plantCfg.get("time", 0))
percent = await cls.getSoilLevelTime(soilInfo.get("soilLevel", 0))
# 处理土地等级带来的时间缩短
time = math.floor(time * (100 + percent) // 100)
matureTs = nowTs + time * 3600
try:
async with cls._transaction():
prev = soilInfo or {}
await cls._deleteUserSoil(uid, soilIndex)
await cls._insertUserSoil(
{
"uid": uid,
"soilIndex": soilIndex,
"plantName": plantName,
"plantTime": nowTs,
"matureTime": matureTs,
"soilLevel": prev.get("soilLevel", 0),
"wiltStatus": 0,
"fertilizerStatus": 0,
"bugStatus": 0,
"weedStatus": 0,
"waterStatus": 0,
"harvestCount": 0,
"isSoilPlanted": 1,
}
)
return True
except Exception as e:
logger.error("播种失败!", e=e)
return False
@classmethod
async def getUserSoilStatus(cls, uid: str, soilIndex: int) -> str:
status = []
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, soilIndex)
if not soilInfo:
return ""
if soilInfo.get("wiltStatus", 0) == 1:
return "枯萎"
if soilInfo.get("fertilizerStatus", 0) == 1:
status.append("施肥")
elif soilInfo.get("fertilizerStatus", 0) == 2:
status.append("增肥")
if soilInfo.get("bugStatus", 0) == 1:
status.append("虫害")
if soilInfo.get("weedStatus", 0) == 1:
status.append("杂草")
if soilInfo.get("waterStatus", 0) == 1:
status.append("缺水")
return ",".join(status)
@classmethod
async def getSoilLevel(cls, level: int) -> str:
"""获取土地等级英文文本
Args:
level (int): 土地等级
Returns:
str:
"""
if level == 1:
return "red"
elif level == 2:
return "black"
elif level == 3:
return "gold"
return "default"
@classmethod
async def getSoilLevelText(cls, level: int) -> str:
"""获取土地等级中文文本
Args:
level (int): 土地等级
Returns:
str:
"""
if level == 1:
return "红土地"
elif level == 2:
return "黑土地"
elif level == 3:
return "金土地"
return "草土地"
@classmethod
async def getSoilLevelHarvestNumber(cls, level: int) -> int:
"""获取土地等级收获数量增加比例
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 2:
return 20
elif level == 3:
return 28
return 10
@classmethod
async def getSoilLevelHarvestExp(cls, level: int) -> int:
"""获取土地等级收获经验增加比例
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 3:
return 28
return 0
@classmethod
async def getSoilLevelTime(cls, level: int) -> int:
"""获取土地等级播种减少时间消耗
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 2:
return 20
elif level == 3:
return 20
return 0
+222
View File
@@ -0,0 +1,222 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserStealDB(CSqlManager):
@classmethod
async def initDB(cls):
userSteal = {
"uid": "TEXT NOT NULL", # 被偷用户Uid
"soilIndex": "INTEGER NOT NULL", # 被偷的地块索引 从1开始
"stealerUid": "TEXT NOT NULL", # 偷菜用户Uid
"stealCount": "INTEGER NOT NULL", # 被偷数量
"stealTime": "INTEGER NOT NULL", # 被偷时间
"PRIMARY KEY": "(uid, soilIndex, stealerUid)",
}
await cls.ensureTableSchema("userSteal", userSteal)
@classmethod
async def addStealRecord(
cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int
) -> bool:
"""添加偷菜记录
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
stealCount (int): 被偷数量
stealTime (int): 被偷时间(时间戳)
Returns:
bool: 操作是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
'INSERT INTO "userSteal"(uid, soilIndex, stealerUid, stealCount, stealTime) VALUES(?, ?, ?, ?, ?);',
(uid, soilIndex, stealerUid, stealCount, stealTime),
)
return True
except Exception as e:
logger.warning("添加偷菜记录失败", e=e)
return False
@classmethod
async def getStealRecordsByUid(cls, uid: str) -> dict:
"""根据用户Uid获取所有偷菜记录
Args:
uid (str): 被偷用户Uid
Returns:
list: 偷菜记录字典列表,每条包含 soilIndex, stealerUid, stealCount, stealTime
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT soilIndex, stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=?;',
(uid,),
)
row = await cursor.fetchone()
if not row:
return {}
result = dict(row)
return result
except Exception as e:
logger.warning("获取偷菜记录失败", e=e)
return {}
@classmethod
async def getStealRecord(cls, uid: str, soilIndex: int) -> list:
"""获取指定地块的所有偷菜记录
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
list: 偷菜记录字典列表,每条包含 stealerUid, stealCount, stealTime
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
rows = await cursor.fetchall()
return [
{
"uid": uid,
"soilIndex": soilIndex,
"stealerUid": row[0],
"stealCount": row[1],
"stealTime": row[2],
}
for row in rows
]
except Exception as e:
logger.warning("获取单地块偷菜记录失败", e=e)
return []
@classmethod
async def getTotalStolenCount(cls, uid: str, soilIndex: int) -> int:
"""计算指定地块被偷的总数量(所有用户偷取数量之和)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
int: 被偷的总数量,如果无记录则返回 0
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT SUM(stealCount) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算总偷菜数量失败", e=e)
return 0
@classmethod
async def getStealerCount(cls, uid: str, soilIndex: int) -> int:
"""计算指定地块被多少人偷过(不同偷菜用户数量)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
int: 偷菜者总数,如果无记录则返回 0
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT COUNT(DISTINCT stealerUid) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算偷菜者数量失败", e=e)
return 0
@classmethod
async def hasStealed(cls, uid: str, soilIndex: int, stealerUid: str) -> bool:
"""判断指定用户是否曾偷取过该地块
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
Returns:
bool: 若存在记录返回 True,否则返回 False
"""
try:
async with cls._transaction():
cursor = await cls.m_pDB.execute(
'SELECT 1 FROM "userSteal" WHERE uid=? AND soilIndex=? AND stealerUid=? LIMIT 1;',
(uid, soilIndex, stealerUid),
)
row = await cursor.fetchone()
return bool(row)
except Exception as e:
logger.warning("检查偷菜记录失败", e=e)
return False
@classmethod
async def updateStealRecord(
cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int
) -> bool:
"""更新偷菜记录的数量和时间
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
stealCount (int): 新的偷菜数量
stealTime (int): 新的偷菜时间(时间戳)
Returns:
bool: 操作是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
'UPDATE "userSteal" SET stealCount=?, stealTime=? WHERE uid=? AND soilIndex=? AND stealerUid=?;',
(stealCount, stealTime, uid, soilIndex, stealerUid),
)
return True
except Exception as e:
logger.warning("更新偷菜记录失败", e=e)
return False
@classmethod
async def deleteStealRecord(cls, uid: str, soilIndex: int) -> bool:
"""删除指定偷菜记录(只需被偷用户Uid和地块索引)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
bool: 删除是否成功
"""
try:
async with cls._transaction():
await cls.m_pDB.execute(
'DELETE FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
return True
except Exception as e:
logger.warning("删除偷菜记录失败", e=e)
return False
+40
View File
@@ -0,0 +1,40 @@
class CDBService:
async def init(self):
from .database.plant import CPlantManager
from .database.user import CUserDB
from .database.userItem import CUserItemDB
from .database.userPlant import CUserPlantDB
from .database.userSeed import CUserSeedDB
from .database.userSign import CUserSignDB
from .database.userSoil import CUserSoilDB
from .database.userSteal import CUserStealDB
self.plant = CPlantManager()
await self.plant.init()
self.user = CUserDB()
await self.user.initDB()
self.userSoil = CUserSoilDB()
await self.userSoil.initDB()
self.userPlant = CUserPlantDB()
await self.userPlant.initDB()
self.userSeed = CUserSeedDB()
await self.userSeed.initDB()
self.userItem = CUserItemDB()
await self.userItem.initDB()
self.userSteal = CUserStealDB()
await self.userSteal.initDB()
self.userSign = CUserSignDB()
await self.userSign.initDB()
async def cleanup(self):
await self.plant.cleanup()
g_pDBService = CDBService()
+1225
View File
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
from pathlib import Path
from jinja2 import Template
from playwright.async_api import async_playwright
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from ..utils.config import g_sResourcePath
class CHelpManager:
@classmethod
def renderHtmlToFile(
cls, path: Path | str, context: dict, output: Path | str
) -> None:
"""
使用 Jinja2 渲染 HTML 模板并保存到指定文件,会自动创建父目录
Args:
path (str): 模板 HTML 路径
context (dict): 用于渲染的上下文字典
output (str): 输出 HTML 文件路径
"""
templatePath = str(path)
outputPath = str(output)
templateStr = Path(templatePath).read_text(encoding="utf-8")
template = Template(templateStr)
rendered = template.render(**context)
# 自动创建目录
Path(outputPath).parent.mkdir(parents=True, exist_ok=True)
Path(outputPath).write_text(rendered, encoding="utf-8")
@classmethod
async def screenshotHtmlToBytes(cls, path: str) -> bytes:
"""
使用 Playwright 截图本地 HTML 文件并返回 PNG 图片字节数据
Args:
path (str): 本地 HTML 文件路径
Returns:
bytes: PNG 图片的原始字节内容
"""
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(
viewport={"width": 1200, "height": 900}, device_scale_factor=1
)
file_url = Path(path).resolve().as_uri()
await page.goto(file_url, wait_until="networkidle")
await page.evaluate("""() => {
return new Promise(r => setTimeout(r, 200));
}""")
image_bytes = await page.screenshot(full_page=True)
await browser.close()
return image_bytes
@classmethod
async def screenshotSave(
cls, path: str, save: str, width: int, height: int
) -> None:
"""
使用 Playwright 渲染本地 HTML 并将截图保存到指定路径
Args:
path (str): HTML 文件路径
save (str): PNG 保存路径(如 output/image.png)
width (int): 图片宽度
height (int): 图片高度
"""
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(
viewport={"width": width, "height": height}, device_scale_factor=1
)
file_url = Path(path).resolve().as_uri()
await page.goto(file_url, wait_until="networkidle")
await page.evaluate("""() => {
return new Promise(r => setTimeout(r, 200));
}""")
# 确保保存目录存在
Path(save).parent.mkdir(parents=True, exist_ok=True)
# 截图并保存到本地文件
await page.screenshot(path=save, full_page=True)
await browser.close()
@classmethod
async def createHelpImage(cls) -> bool:
templatePath = g_sResourcePath / "html/help.html"
outputPath = g_sResourcePath / "temp_html/help.html"
savePath = DATA_PATH / "farm_res/html/help.png"
context = {
"main_title": "真寻农场帮助菜单",
"subtitle": "[]中为可选参数",
"page_title": "真寻农场帮助菜单",
"font_family": "MyFont",
"contents": [
{"title": "主要指令", "commands": ["指令A", "指令B"]},
{"title": "B", "commands": ["指令D", "指令E", "指令M", "指令i"]},
],
}
try:
cls.renderHtmlToFile(templatePath, context, outputPath)
bytes = await cls.screenshotSave(str(outputPath), str(savePath), 1500, 2300)
except Exception as e:
logger.warning("绘制农场帮助菜单失败", e=e)
return False
return True
g_pHelpManager = CHelpManager()
+219
View File
@@ -0,0 +1,219 @@
from ..dbService import g_pDBService
class CPlayer:
def __init__(self):
self.user = {
"uid": "", # 用户Uid
"name": "", # 农场名称
"exp": 0, # 经验值
"point": 0, # 金币
"vipPoint": 0, # 点券
"soil": 3, # 解锁土地数量
"stealTime": "", # 偷菜时间字符串
"stealCount": 0, # 剩余偷菜次数
}
async def init(self, uid: str) -> bool:
self.user["uid"] = uid
return await self.loadFormDB()
async def loadFormDB(self) -> bool:
uid = self.user.get("uid", "")
if uid == "":
return False
self.user = await g_pDBService.user.getUserInfoByUid(uid)
return True
async def isRegistered(self) -> bool:
"""检查用户是否注册农场
Returns:
bool: 是否注册农场
"""
uid = self.user.get("uid", "")
if uid == "":
return False
return await g_pDBService.user.isRegistered(uid)
async def addPoint(self, type: str, index: int) -> bool:
"""增加货币
Args:
type (str): 货币类型 point/vipPoint
index (int): 增加的数量
Returns:
bool: 是否成功增加货币
"""
uid = self.user.get("uid", "")
if uid == "" or type not in g_pDBService.user.currencies:
return False
if index == 0:
return True
nowIndex = self.user.get(type, 0) + index
if nowIndex < 0:
nowIndex = 0
if await g_pDBService.user.updatePoint(uid, type, nowIndex):
self.user[type] = nowIndex
return True
return False
async def subPoint(self, type: str, index: int) -> bool:
"""减少货币
Args:
type (str): 货币类型 point/vipPoint
index (int): 减少的数量
Returns:
bool: 是否成功减少货币
"""
uid = self.user.get("uid", "")
if uid == "" or type not in g_pDBService.user.currencies:
return False
if index == 0:
return True
nowIndex = self.user.get(type, 0) - index
if nowIndex < 0:
nowIndex = 0
if await g_pDBService.user.updatePoint(uid, type, nowIndex):
self.user[type] = nowIndex
return True
return False
async def addExp(self, exp: int) -> bool:
"""增加经验值
Args:
exp (int): 增加的经验值
Returns:
bool: 是否成功增加经验值
"""
uid = self.user.get("uid", "")
if uid == "":
return False
if exp == 0:
return True
nowExp = self.user.get("exp", 0) + exp
if nowExp < 0:
nowExp = 0
if await g_pDBService.user.updateExp(uid, nowExp):
self.user["exp"] = nowExp
return True
return False
async def subExp(self, exp: int) -> bool:
"""减少经验值
Args:
exp (int): 减少的经验值
Returns:
bool: 是否成功减少经验值
"""
uid = self.user.get("uid", "")
if uid == "":
return False
if exp == 0:
return True
nowExp = self.user.get("exp", 0) - exp
if nowExp < 0:
nowExp = 0
if await g_pDBService.user.updateExp(uid, nowExp):
self.user["exp"] = nowExp
return True
return False
async def updateName(self, name: str) -> str:
"""更新农场名称
Args:
name (str): 农场名称
Returns:
str: success/error/error1
"""
uid = self.user.get("uid", "")
if uid == "":
return "error"
return await g_pDBService.user.updateName(uid, name)
async def getUserLevel(self) -> tuple[int, int, int]:
"""获取用户等级信息
Returns:
tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验)
失败返回(-1, -1, -1)
"""
uid = self.user.get("uid", "")
if uid == "":
return -1, -1, -1
return await g_pDBService.user.getUserLevelByUid(uid)
async def updateStealCountByUid(
self, uid: str, stealTime: str, stealCount: int
) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
stealTime (str): 偷菜日期
stealCount (int): 新剩余偷菜次数
Returns:
bool: 是否更新成功
"""
uid = self.user.get("uid", "")
if uid == "":
return False
return await g_pDBService.user.updateStealCountByUid(uid, stealTime, stealCount)
async def updateField(self, field: str, value) -> bool:
"""更新单字段信息
Returns:
bool: 是否成功更新单字段信息
"""
uid = self.user.get("uid", "")
if uid == "":
return False
return await g_pDBService.user.updateFieldByUid(uid, field, value)
+216
View File
@@ -0,0 +1,216 @@
import threading
import time
from typing import Any
from zhenxun.services.log import logger
class CPlayerPool:
"""
用户池管理类
管理用户对象的生命周期,支持自动清理超时用户
"""
def __init__(self, timeoutSeconds: int = 300, cleanupInterval: int = 3600):
"""
初始化用户池
Args:
timeoutSeconds: 用户超时时间(秒),默认5分钟
cleanupInterval: 清理间隔(秒),默认1小时
"""
self._players: dict[str, dict[str, Any]] = {}
self._lock = threading.RLock()
self.timeoutSeconds = timeoutSeconds
self.cleanupInterval = cleanupInterval
# 启动后台清理线程
self._cleanupThread = threading.Thread(target=self._cleanupWorker, daemon=True)
self._running = True
self._cleanupThread.start()
logger.debug(
f"用户池初始化完成,超时时间: {timeoutSeconds}秒, 清理间隔: {cleanupInterval}秒"
)
def createUser(self, uid: str, userObj: Any) -> bool:
"""
创建并管理用户对象
Args:
uid: 用户ID
userObj: 用户对象
Returns:
bool: 是否创建成功
"""
with self._lock:
if uid in self._players:
logger.debug(f"用户 {uid} 已存在,正在覆盖")
# 可以选择返回False或者覆盖,这里选择覆盖
# return False
self._players[uid] = {
"object": userObj,
"lastActive": time.time(),
"activeCount": 0,
}
logger.debug(f"用户 {uid} 创建并开始管理")
return True
def getUser(self, uid: str) -> Any | None:
"""
获取用户对象并刷新活跃时间
Args:
uid: 用户ID
Returns:
Optional[Any]: 用户对象,如果不存在或已超时则返回None
"""
with self._lock:
userData = self._players.get(uid)
if not userData:
logger.debug(f"用户 {uid} 不存在")
return None
# 检查是否已超时(防御性检查)
currentTime = time.time()
if currentTime - userData["lastActive"] > self.timeoutSeconds:
logger.debug(f"用户 {uid} 在获取操作期间已超时")
self._removeUser(uid)
return None
# 刷新活跃时间
userData["lastActive"] = currentTime
userData["activeCount"] += 1
logger.debug(f"用户 {uid} 获取成功,活跃次数: {userData['activeCount']}")
return userData["object"]
def updateUser(self, uid: str, userObj: Any) -> bool:
"""
更新用户对象
Args:
uid: 用户ID
userObj: 新的用户对象
Returns:
bool: 是否更新成功
"""
with self._lock:
if uid not in self._players:
logger.debug(f"用户 {uid} 不存在,无法更新")
return False
self._players[uid]["object"] = userObj
self._players[uid]["lastActive"] = time.time()
logger.debug(f"用户 {uid} 更新成功")
return True
def removeUser(self, uid: str) -> bool:
"""
主动移除用户
Args:
uid: 用户ID
Returns:
bool: 是否移除成功
"""
with self._lock:
return self._removeUser(uid)
def _removeUser(self, uid: str) -> bool:
"""内部移除用户方法"""
if uid in self._players:
userData = self._players.pop(uid)
# 如果需要清理资源,可以在这里处理
if hasattr(userData["object"], "close"):
try:
userData["object"].close()
except Exception as e:
logger.debug(f"关闭用户 {uid} 时出错: {e}")
logger.debug(f"用户 {uid} 已移除,总活跃次数: {userData['activeCount']}")
return True
return False
def _cleanupWorker(self):
"""后台清理线程的工作函数"""
while self._running:
try:
self._cleanupExpiredUsers()
except Exception as e:
logger.debug(f"清理工作线程出错: {e}")
# 休眠指定间隔
time.sleep(self.cleanupInterval)
def _cleanupExpiredUsers(self):
"""清理超时用户"""
currentTime = time.time()
expiredUsers = []
# 首先收集过期的用户ID,避免在迭代中修改字典
with self._lock:
for uid, userData in self._players.items():
if currentTime - userData["lastActive"] > self.timeoutSeconds:
expiredUsers.append(uid)
# 移除过期用户
for uid in expiredUsers:
with self._lock:
# 再次检查,防止在收集和移除之间用户被更新
if (
uid in self._players
and currentTime - self._players[uid]["lastActive"]
> self.timeoutSeconds
):
self._removeUser(uid)
if expiredUsers:
logger.debug(f"已清理 {len(expiredUsers)} 个过期用户: {expiredUsers}")
def getActiveUsers(self) -> dict[str, dict[str, Any]]:
"""
获取当前活跃用户信息
Returns:
Dict: 用户信息字典
"""
with self._lock:
# 返回副本避免外部修改
return {
uid: {
"lastActive": data["lastActive"],
"activeCount": data["activeCount"],
"timeRemaining": self.timeoutSeconds
- (time.time() - data["lastActive"]),
}
for uid, data in self._players.items()
}
def userCount(self) -> int:
"""获取当前用户数量"""
with self._lock:
return len(self._players)
def shutdown(self):
"""关闭用户池,清理资源"""
self._running = False
if self._cleanupThread.is_alive():
self._cleanupThread.join(timeout=5)
# 清理所有用户
with self._lock:
uids = list(self._players.keys())
for uid in uids:
self._removeUser(uid)
logger.debug("用户池关闭完成")
g_pUserPool = CPlayerPool()
+245
View File
@@ -0,0 +1,245 @@
import math
from zhenxun.utils.image_utils import ImageTemplate
from ..core.dbService import g_pDBService
from ..utils.config import g_sResourcePath, g_sTranslation
from ..utils.tool import g_pToolManager
class CShopManager:
@classmethod
async def getSeedShopImage(
cls, filterKey: str | int = 1, num: int = 1, isVip: int = 0
) -> bytes:
"""获取商店页面
Args:
filterKey (str|int):
- 字符串: 根据关键字筛选种子名称
- 整数: 翻至对应页(无筛选)
num (int, optional): 当 filterKey 为字符串时,用于指定页码。Defaults to 1.
Returns:
bytes: 返回商店图片bytes
"""
# 解析参数:区分筛选关键字和页码
filterStr = None
if isinstance(filterKey, int):
page = filterKey
else:
filterStr = filterKey
page = num
# 表头定义
columnName = [
"-",
"种子名称",
"农场币",
"解锁等级",
"果实单价",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"是否可以上架交易行",
]
# 查询所有可购买作物,并根据筛选关键字过滤
plants = await g_pDBService.plant.listPlants()
filteredPlants = []
# 如果是点券商店
if isVip:
columnName[2] = "点券"
for plant in plants:
# 只留下点券购买的种子
if plant["isVip"] == 0:
continue
# 跳过未解锁购买的种子
if plant["isBuy"] == 0:
continue
# 字符串筛选
if filterStr and filterStr not in plant["name"]:
continue
filteredPlants.append(plant)
else:
for plant in plants:
# 只留下农场币购买的种子
if plant["isVip"] == 1:
continue
# 跳过未解锁购买的种子
if plant["isBuy"] == 0:
continue
# 字符串筛选
if filterStr and filterStr not in plant["name"]:
continue
filteredPlants.append(plant)
# 计算分页
totalCount = len(filteredPlants)
pageCount = math.ceil(totalCount / 15) if totalCount else 1
startIndex = (page - 1) * 15
pageItems = filteredPlants[startIndex : startIndex + 15]
# 构建数据行
dataList = []
for plant in pageItems:
# 图标处理
icon = ""
iconPath = g_sResourcePath / f"plant/{plant['name']}/icon.png"
if iconPath.exists():
icon = (iconPath, 33, 33)
# 交易行标记
sell = "可以" if plant["sell"] else "不可以"
dataList.append(
[
icon,
plant["name"], # 种子名称
plant["buy"], # 农场币种子单价
plant["level"], # 解锁等级
plant["price"], # 果实单价
plant["experience"], # 收获经验
plant["harvest"], # 收获数量
plant["time"], # 成熟时间(小时)
plant["crop"], # 收获次数
sell, # 是否可上架交易行
]
)
if isVip:
dataList[-1][2] = plant["vipBuy"] # 点券种子单价
# 页码标题
title = f"种子商店 页数: {page}/{pageCount}"
# 渲染表格并返回图片bytes
result = await ImageTemplate.table_page(
title,
"购买示例:@小真寻 购买种子 大白菜 5",
columnName,
dataList,
)
return result.pic2bytes()
@classmethod
async def buySeed(cls, uid: str, name: str, num: int = 1) -> str:
"""购买种子
Args:
uid (str): 用户Uid
name (str): 植物名称
num (int, optional): 购买数量
Returns:
str:
"""
if num <= 0:
return g_sTranslation["buySeed"]["notNum"]
player = await g_pToolManager.getPlayerByUid(uid)
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo or not player:
return g_sTranslation["buySeed"]["error"]
level = player.user.get("level", 0)
if level < int(plantInfo["level"]):
return g_sTranslation["buySeed"]["noLevel"]
vipSeed = plantInfo.get("isVip", 0) == 1
currencyType = "vipPoint" if vipSeed else "point"
price = int(plantInfo["vipBuy" if vipSeed else "buy"])
totalCost = price * num
currentCurrency = player.user.get(currencyType, 0)
if currentCurrency < totalCost:
return g_sTranslation["buySeed"][f"no{'Vip' if vipSeed else ''}Point"]
await player.addPoint(currencyType, currentCurrency - totalCost)
if not await g_pDBService.userSeed.addUserSeedByUid(uid, name, num):
return g_sTranslation["buySeed"]["errorSql"]
success_key = "vipSuccess" if vipSeed else "success"
remaining_currency = currentCurrency - totalCost
return g_sTranslation["buySeed"][success_key].format(
name=name, total=totalCost, point=remaining_currency
)
@classmethod
async def sellPlantByUid(cls, uid: str, name: str = "", num: int = 1) -> str:
"""出售作物
Args:
uid (str): 用户Uid
Returns:
str:
"""
if not isinstance(name, str) or name.strip() == "":
name = ""
plant = await g_pDBService.userPlant.getUserPlantByUid(uid)
if not plant:
return g_sTranslation["sellPlant"]["no"]
point = 0
totalSold = 0
isAll = num == -1
if name == "":
for plantName, count in plant.items():
isLock = await g_pDBService.userPlant.checkPlantLockByName(
uid, plantName
)
if isLock:
continue
plantInfo = await g_pDBService.plant.getPlantByName(plantName)
if not plantInfo:
continue
point += plantInfo["price"] * count
await g_pDBService.userPlant.updateUserPlantByName(uid, plantName, 0)
else:
if name not in plant:
return g_sTranslation["sellPlant"]["error"].format(name=name)
available = plant[name]
sellAmount = available if isAll else min(available, num)
if sellAmount <= 0:
return g_sTranslation["sellPlant"]["error1"].format(name=name)
await g_pDBService.userPlant.updateUserPlantByName(
uid, name, available - sellAmount
)
totalSold = sellAmount
if name == "":
totalPoint = point
else:
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
price = 0
else:
price = plantInfo["price"]
totalPoint = totalSold * price
player = await g_pToolManager.getPlayerByUid(uid)
if not player:
return g_sTranslation["basic"]["error"]
currentPoint = player.user.get("point", 0)
await player.addPoint("point", currentPoint + totalPoint)
result = "success1" if name == "" else "success"
return g_sTranslation["sellPlant"][result].format(
point=totalPoint, num=currentPoint + totalPoint
)
g_pShopManager = CShopManager()
-649
View File
@@ -1,649 +0,0 @@
import math
import os
import re
from datetime import date, datetime, timedelta
from io import StringIO
from math import e
from typing import Any, Dict, List, Optional
import aiosqlite
from zhenxun.services.log import logger
from .config import g_pJsonManager, g_sDBFilePath, g_sDBPath
class CSqlManager:
def __init__(self):
g_sDBPath.mkdir(parents=True, exist_ok=True)
@classmethod
async def cleanup(cls):
if cls.m_pDB:
await cls.m_pDB.close()
@classmethod
async def init(cls) -> bool:
bIsExist = os.path.exists(g_sDBFilePath)
cls.m_pDB = await aiosqlite.connect(g_sDBFilePath)
#if bIsExist == False:
#TODO 缺少判断创建失败事件
#await cls.createDB()
await cls.checkDB()
return True
@classmethod
async def getColumns(cls, tableName):
""" 由AI生成
获取表的列信息
"""
try:
cursor = await cls.m_pDB.execute(f'PRAGMA table_info("{tableName}")')
columns = [row[1] for row in await cursor.fetchall()]
return columns
except aiosqlite.Error as e:
logger.error(f"获取表结构失败: {str(e)}")
raise
@classmethod
async def ensure_table_exists(cls, tableName, columns) -> bool:
"""智能创建并分析数据库表、字段是否存在 由AI生成
Args:
tableName (_type_): 表名
columns (_type_): 字典
Returns:
_type_: _description_
"""
try:
current_columns = await cls.getColumns(tableName)
#检查表是否存在
table_exists = bool(current_columns)
#如果表不存在,直接创建
if not table_exists:
create_sql = f'''
CREATE TABLE "{tableName}" (
{", ".join(f'"{k}" {v}' for k, v in columns.items())}
);
'''
await cls.m_pDB.execute(create_sql)
await cls.m_pDB.commit() #显式提交新建表操作
return True
#表存在时的处理
columns_to_add = []
columns_to_remove = []
#检查需要添加的列
for k, v in columns.items():
if k not in current_columns:
columns_to_add.append(f'"{k}" {v}')
#检查需要移除的列
for col in current_columns:
if col not in columns.keys():
columns_to_remove.append(col)
#执行修改
if columns_to_add or columns_to_remove:
try:
#开启事务(使用connection级别的事务控制)
await cls.m_pDB.execute('BEGIN TRANSACTION')
#添加新列
for col_def in columns_to_add:
await cls.m_pDB.execute(f'ALTER TABLE "{tableName}" ADD COLUMN {col_def}')
#删除旧列
for col in columns_to_remove:
await cls.m_pDB.execute(f'ALTER TABLE "{tableName}" DROP COLUMN "{col}"')
#显式提交事务
await cls.m_pDB.commit()
return True
except Exception as e:
#回滚事务
await cls.m_pDB.rollback()
logger.error(f"表结构迁移失败: {str(e)}")
return False
except aiosqlite.Error as e:
logger.error(f"表结构迁移失败: {str(e)}")
return True
@classmethod
async def checkDB(cls) -> bool:
userInfo = {
"uid": "INTEGER PRIMARY KEY AUTOINCREMENT",
"name": "TEXT NOT NULL",
"exp": "INTEGER DEFAULT 0",
"point": "INTEGER DEFAULT 0",
"soil": "INTEGER DEFAULT 3",
"stealing": "TEXT DEFAULT NULL"
}
userStorehouse = {
"uid": "INTEGER PRIMARY KEY AUTOINCREMENT",
"item": "TEXT DEFAULT ''",
"plant": "TEXT DEFAULT ''",
"seed": "TEXT DEFAULT ''"
}
userSoilInfo = {
"uid": "INTEGER PRIMARY KEY AUTOINCREMENT",
**{f"soil{i}": "TEXT DEFAULT ''" for i in range(1, 31)}
}
await cls.ensure_table_exists("user", userInfo)
await cls.ensure_table_exists("storehouse", userStorehouse)
await cls.ensure_table_exists("soil", userSoilInfo)
return True
@classmethod
async def executeDB(cls, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if len(command) <= 0:
logger.warning("数据库语句长度不能!")
return False
try:
await cls.m_pDB.execute(command)
await cls.m_pDB.commit()
return True
except Exception as e:
logger.warning("数据库语句执行出错:" + command)
return False
@classmethod
async def initUserInfoByUid(cls, uid: str, name: str = "", exp: int = 0, point: int = 100):
"""初始化用户信息
Args:
uid (str): 用户Uid
name (str): 农场名称
exp (int): 农场经验
point (int): 农场币
"""
#用户信息
userInfo = f"""
INSERT INTO user (uid, name, exp, point, soil, stealing) VALUES ({uid}, '{name}', {exp}, {point}, 3, '{date.today()}|5')
"""
#用户仓库
userStorehouse = f"""
INSERT INTO storehouse (uid) VALUES ({uid});
"""
#用户土地
userSoilInfo = f"""
INSERT INTO soil (uid) VALUES ({uid});
"""
if not await cls.executeDB(userInfo):
return False
if not await cls.executeDB(userStorehouse):
return False
if not await cls.executeDB(userSoilInfo):
return False
return "开通农场成功"
@classmethod
async def getUserInfoByUid(cls, uid: str) -> dict:
"""根据用户Uid获取用户信息
Args:
uid (str): 用户Uid
Returns:
list[dict]: 用户信息
"""
if len(uid) <= 0:
return {}
try:
async with cls.m_pDB.execute(
"SELECT * FROM user WHERE uid = ?", (uid,)
) as cursor:
async for row in cursor:
userDict = {
"uid": row[0],
"name": row[1],
"exp": row[2],
"point": row[3],
"soil": row[4],
"stealing": row[5]
}
return userDict
return {}
except Exception as e:
logger.warning(f"getUserInfoByUid查询失败: {e}")
return {}
@classmethod
async def getUserPointByUid(cls, uid: str) -> int:
"""根据用户Uid获取用户农场币
Args:
uid (str): 用户Uid
Returns:
int: 用户农场币
"""
if len(uid) <= 0:
return -1
try:
async with cls.m_pDB.execute(f"SELECT point FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
return int(row[0])
return -1
except Exception as e:
logger.warning(f"getUserPointByUid查询失败: {e}")
return -1
@classmethod
async def updateUserPointByUid(cls, uid: str, point: int) -> int:
"""根据用户Uid修改用户农场币
Args:
uid (str): 用户Uid
point (int): 要更新的新农场币数量(需 ≥ 0)
Returns:
int: 更新后的农场币数量(成功时),-1(失败时)
"""
if len(uid) <= 0:
logger.warning("参数校验失败: uid为空或农场币值无效")
return -1
try:
return await cls.executeDB(f"UPDATE user SET point = {point} WHERE uid = {uid}")
except Exception as e:
logger.error(f"金币更新失败: {e}")
return -1
@classmethod
async def getUserExpByUid(cls, uid: str) -> int:
"""根据用户Uid获取用户经验
Args:
uid (str): 用户Uid
Returns:
int: 用户经验值
"""
if len(uid) <= 0:
return -1
try:
async with cls.m_pDB.execute(f"SELECT exp FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
return int(row[0])
return -1
except Exception as e:
logger.warning(f"getUserLevelByUid查询失败: {e}")
return -1
@classmethod
async def UpdateUserExpByUid(cls, uid: str, exp: int) -> bool:
"""根据用户Uid刷新用户经验
Args:
uid (str): 用户Uid
Returns:
bool: 是否成功
"""
if len(uid) <= 0:
return False
sql = f"UPDATE user SET exp = '{exp}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def getUserLevelByUid(cls, uid: str) -> tuple[int, int, int]:
"""根据用户Uid获取用户等级
Args:
uid (str): 用户Uid
Returns:
tuple[int, int, int]: (当前等级, 下级所需经验, 当前等级剩余经验)
"""
if len(uid) <= 0:
return -1, -1, -1
try:
async with cls.m_pDB.execute(f"SELECT exp FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
exp = int(row[0])
level = exp // 200
nextLevelExp = 200 * (level + 1)
currentLevelExp = level * 200
remainingExp = exp - currentLevelExp
return level, nextLevelExp, remainingExp
return -1, -1, -1
except Exception as e:
logger.warning(f"getUserLevelByUid查询失败: {e}")
return -1, -1, -1
@classmethod
async def getUserSoilByUid(cls, uid: str) -> int:
"""根据用户Uid获取解锁地块
Args:
uid (str): 用户Uid
Returns:
int: 解锁几块地
"""
if len(uid) <= 0:
return 0
async with cls.m_pDB.execute(f"SELECT soil FROM user WHERE uid = {uid}") as cursor:
async for row in cursor:
if not row[0]:
return 0
else:
return int(row[0])
return 0
@classmethod
async def getUserSoilStatusBySoilID(cls, uid: str, soil: str) -> tuple[bool, str]:
"""根据土地块获取用户土地状态
Args:
uid (str): 用户Uid
soil (str): 土地id
Returns:
tuple[bool, str]: [是否可以播种,土地信息]
"""
if len(uid) <= 0:
return False, ""
async with cls.m_pDB.execute(f"SELECT {soil} FROM soil WHERE uid = {uid}") as cursor:
async for row in cursor:
if row[0] == None or len(row[0]) <= 0:
return True, ""
else:
return False, row[0]
return False, ""
@classmethod
async def updateUserSoilStatusByPlantName(cls, uid: str, soil: str,
plant: str = "",
status: int = 0) -> bool:
"""根据种子名称使用户播种
Args:
uid (str): 用户Uid
soil (str): 土地id
plant (str): 种子名称
Returns:
bool: 是否更新成功
"""
if len(uid) <= 0:
return False
if len(plant) <= 0 and status == 4:
s = f",,,{status},"
elif len(plant) <= 0 and status != 4:
s = ""
else:
#获取种子信息 这里能崩我吃
plantInfo = g_pJsonManager.m_pPlant['plant'][plant]
currentTime = datetime.now()
newTime = currentTime + timedelta(hours=int(plantInfo['time']))
#0: 种子名称
#1: 种下时间
#2: 预计成熟时间
#3: 地状态:0:无 1:长草 2:生虫 3:缺水 4:枯萎
#4: 是否被偷 示例:QQ号-偷取数量|QQ号-偷取数量
#5: 土地等级 0:普通 1:红土地 2:黑土地 3:金土地 4:紫晶土地 5:蓝晶土地 6:黑晶土地
s = f"{plant},{int(currentTime.timestamp())},{int(newTime.timestamp())},{status},,"
sql = f"UPDATE soil SET {soil} = '{s}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def getUserSeedByUid(cls, uid: str) -> str:
"""获取用户仓库种子信息
Args:
info (list[dict]): 用户信息
Returns:
str: 仓库种子信息
"""
if len(uid) <= 0:
return ""
try:
async with cls.m_pDB.execute(f"SELECT seed FROM storehouse WHERE uid = {uid}") as cursor:
async for row in cursor:
return row[0]
return ""
except Exception as e:
logger.warning(f"getUserSeedByUid查询失败: {e}")
return ""
@classmethod
async def getUserSeedByName(cls, uid: str, name: str) -> int:
"""获取用户仓库种子信息
Args:
uid (str): 用户信息
name (str): 种子名称
Returns:
int: 仓库种子信息
"""
if len(uid) <= 0:
return -1
try:
async with cls.m_pDB.execute(f"SELECT seed FROM storehouse WHERE uid = {uid}") as cursor:
async for row in cursor:
if row[0] == None or len(row[0]) == 0:
return -1
plantDict: Dict[str, int] = {}
for item in row[0].split(','):
if '|' in item:
seedName, count = item.split('|', 1)
plantDict[seedName] = int(count)
if name in plantDict:
return plantDict[name]
else:
return -1
return -1
except Exception as e:
logger.warning(f"getUserSeedByUid查询失败: {e}")
return -1
@classmethod
async def updateUserSeedByUid(cls, uid: str, seed: str) -> bool:
"""更新用户种子仓库
Args:
uid (str): 用户Uid
seed (str): 种子名称
Returns:
bool:
"""
if len(uid) <= 0:
return False
sql = f"UPDATE storehouse SET seed = '{seed}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def addUserSeedByPlant(cls, uid: str, seed: str, num: int) -> bool:
"""添加作物信息至仓库
Args:
uid (str): 用户Uid
seed (str): 种子名称
num(str): 种子数量
Returns:
bool:
"""
if len(uid) <= 0:
return False
seedsDict = {}
currentSeeds = await cls.getUserSeedByUid(uid)
if currentSeeds:
for item in currentSeeds.split(','):
if item.strip():
name, count = item.split('|')
seedsDict[name.strip()] = int(count.strip())
if seed in seedsDict:
seedsDict[seed] += num
if seedsDict[seed] <= 0:
del seedsDict[seed]
else:
if num > 0:
seedsDict[seed] = num
updatedSeeds = ','.join([f"{name}|{count}" for name, count in seedsDict.items()])
sql = f"UPDATE storehouse SET seed = '{updatedSeeds}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def getUserPlantByUid(cls, uid: str) -> str:
"""获取用户仓库作物信息
Args:
info (list[dict]): 用户信息
Returns:
str: 仓库作物信息
"""
if len(uid) <= 0:
return ""
try:
async with cls.m_pDB.execute(f"SELECT plant FROM storehouse WHERE uid = {uid}") as cursor:
async for row in cursor:
return row[0]
return ""
except Exception as e:
logger.warning(f"getUserPlantByUid查询失败: {e}")
return ""
@classmethod
async def updateUserPlantByUid(cls, uid: str, plant: str) -> bool:
"""更新用户作物仓库
Args:
uid (str): 用户Uid
plant (str): 作物名称
Returns:
bool:
"""
if len(uid) <= 0:
return False
sql = f"UPDATE storehouse SET plant = '{plant}' WHERE uid = {uid}"
return await cls.executeDB(sql)
@classmethod
async def addUserPlantByPlant(cls, uid: str, plant: str, num: int) -> bool:
"""添加作物信息至仓库
Args:
uid (str): 用户Uid
plant (str): 作物名称
num(str): 作物数量
Returns:
bool:
"""
if len(uid) <= 0:
return False
plantsDict = {}
currentPlants = await cls.getUserPlantByUid(uid)
if currentPlants:
for item in currentPlants.split(','):
if item.strip():
name, count = item.split('|')
plantsDict[name.strip()] = int(count.strip())
if plant in plantsDict:
plantsDict[plant] += num
if plantsDict[plant] <= 0:
del plantsDict[plant]
else:
if num > 0:
plantsDict[plant] = num
updatedPlants = ','.join([f"{name}|{count}" for name, count in plantsDict.items()])
sql = f"UPDATE storehouse SET plant = '{updatedPlants}' WHERE uid = {uid}"
return await cls.executeDB(sql)
g_pSqlManager = CSqlManager()
+123
View File
@@ -0,0 +1,123 @@
import inspect
import time
from zhenxun.services.log import logger
class Signal:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
bound = instance.__dict__.get(self.name)
if bound is None:
bound = _SignalBound()
instance.__dict__[self.name] = bound
return bound
class _SignalBound:
def __init__(self):
self._slots = []
self._onceSlots = []
def connect(self, func=None, *, priority=0):
if func is None:
return lambda f: self.connect(f, priority=priority)
if callable(func) and not any(s[0] == func for s in self._slots):
self._slots.append((func, priority))
self._slots.sort(key=lambda x: -x[1])
return func
def connect_once(self, func=None, *, priority=0):
if func is None:
return lambda f: self.connect_once(f, priority=priority)
if callable(func) and not any(s[0] == func for s in self._onceSlots):
self._onceSlots.append((func, priority))
self._onceSlots.sort(key=lambda x: -x[1])
return func
def disconnect(self, func):
self._slots = [s for s in self._slots if s[0] != func]
self._onceSlots = [s for s in self._onceSlots if s[0] != func]
async def emit(self, *args, **kwargs):
slots = list(self._slots)
onceSlots = list(self._onceSlots)
self._onceSlots.clear()
for slot, _ in slots + onceSlots:
start = time.time()
try:
if inspect.iscoroutinefunction(slot):
await slot(*args, **kwargs)
else:
slot(*args, **kwargs)
logger.debug(
f"【真寻农场】事件槽 {slot.__name__} 执行完成,耗时 {(time.time() - start) * 1000:.2f} ms"
)
except Exception as e:
logger.warning(f"事件槽 {slot.__name__} 触发异常: {e}")
class FarmEventManager:
m_beforePlant = Signal()
"""播种前信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int): 播种数量
"""
m_afterPlant = Signal()
"""播种后信号 每块地播种都会触发该信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
soilIndex (int): 播种地块索引 从1开始
"""
m_beforeHarvest = Signal()
"""收获前信号
Args:
uid (str): 用户Uid
"""
m_afterHarvest = Signal()
"""收获后信号 每块地收获都会触发该信号
Args:
uid (str): 用户Uid
name (str): 收获作物名称
num (int): 收获数量
soilIndex (int): 收获地块索引 从1开始
"""
m_beforeEradicate = Signal()
"""铲除前信号
Args:
uid (str): 用户Uid
"""
m_afterEradicate = Signal()
"""铲除后信号 每块地铲除都会触发该信号
Args:
uid (str): 用户Uid
soilIndex (index): 铲除地块索引 从1开始
"""
m_beforeExpand = Signal()
m_afterExpand = Signal()
m_beforeSteal = Signal()
m_afterSteal = Signal()
m_dit = Signal()
g_pEventManager = FarmEventManager()
-723
View File
@@ -1,723 +0,0 @@
import asyncio
import math
import random
from datetime import date, datetime
from io import StringIO
from typing import Dict, List, Tuple
from numpy import number
from zhenxun.configs.config import Config
from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.enum import GoldHandle
from zhenxun.utils.image_utils import ImageTemplate
from zhenxun.utils.platform import PlatformUtils
from ..config import g_pJsonManager, g_sResourcePath
from ..database import g_pSqlManager
class CFarmManager:
@classmethod
async def buyPointByUid(cls, uid: str, num: int) -> str:
if num <= 0:
return "你是怎么做到购买不是正数的农场币的"
user = await UserConsole.get_user(uid)
pro = float(Config.get_config("zhenxun_plugin_farm", "兑换倍数"))
tax = float(Config.get_config("zhenxun_plugin_farm", "手续费"))
#计算手续费
fee = math.floor(num * tax)
#实际扣费金额
deduction = num + fee
if user.gold < deduction:
return f"你的金币不足或不足承担手续费。当前手续费为{fee}"
await UserConsole.reduce_gold(uid, num, GoldHandle.PLUGIN , 'zhenxun_plugin_farm')
await UserConsole.reduce_gold(uid, fee, GoldHandle.PLUGIN , 'zhenxun_plugin_farm')
point = num * pro
p = await g_pSqlManager.getUserPointByUid(uid)
number = point + p
await g_pSqlManager.updateUserPointByUid(uid, int(number))
return f"充值{point}农场币成功,手续费{tax}金币,当前农场币:{number}"
@classmethod
async def drawFarmByUid(cls, uid: str, name: str) -> bytes:
"""绘制用户农场
Args:
uid (str): 用户UID
Returns:
bytes: 返回绘制结果
"""
img = BuildImage(background = g_sResourcePath / "background/background.jpg")
soilSize = g_pJsonManager.m_pSoil['size']
#TODO 缺少判断用户土地资源状况
soil = BuildImage(background = g_sResourcePath / "soil/普通土地.png")
await soil.resize(0, soilSize[0], soilSize[1])
grass = BuildImage(background = g_sResourcePath / "soil/草土地.png")
await grass.resize(0, soilSize[0], soilSize[1])
soilPos = g_pJsonManager.m_pSoil['soil']
soilUnlock = await g_pSqlManager.getUserSoilByUid(uid)
x = 0
y = 0
isFirstExpansion = True #首次添加扩建图片
isFirstRipe = True
plant = None
for index in range(0, 30):
x = soilPos[str(index + 1)]['x']
y = soilPos[str(index + 1)]['y']
#如果土地已经到达对应等级
if index < soilUnlock:
await img.paste(soil, (x, y))
isPlant, plant, isRipe= await cls.drawSoilPlant(uid, f"soil{str(index + 1)}")
if isPlant:
await img.paste(plant, (x + soilSize[0] // 2 - plant.width // 2,
y + soilSize[1] // 2 - plant.height // 2))
#1700 275
#首次添加可收获图片
if isRipe and isFirstRipe:
ripe = BuildImage(background = g_sResourcePath / "background/ripe.png")
await img.paste(ripe, (x + soilSize[0] // 2 - ripe.width // 2,
y - ripe.height // 2))
isFirstRipe = False
else:
await img.paste(grass, (x, y))
if isFirstExpansion:
isFirstExpansion = False
#首次添加扩建图片
expansion = BuildImage(background = g_sResourcePath / "background/expansion.png")
await expansion.resize(0, 69, 69)
await img.paste(expansion, (x + soilSize[0] // 2 - expansion.width // 2,
y + soilSize[1] // 2 - expansion.height))
#左上角绘制用户信息
#头像
image = await PlatformUtils.get_user_avatar(uid, "qq")
if image:
avatar = BuildImage(background = image)
await img.paste(avatar, (125, 85))
#头像框
frame = BuildImage(background = g_sResourcePath / "background/frame.png")
await img.paste(frame, (75, 44))
#用户名
nameImg = await BuildImage.build_text_image(name, size = 24, font_color = (77, 35, 4))
await img.paste(nameImg, (300, 92))
#经验值
level = await g_pSqlManager.getUserLevelByUid(uid)
beginX = 309
endX = 627
#绘制宽度计算公式为 (当前经验值 / 经验值上限) * 宽度
width = int((level[2] / level[1]) * (endX - beginX))
await img.rectangle((beginX, 188, beginX + width, 222), (171, 194, 41))
expImg = await BuildImage.build_text_image(f"{level[2]} / {level[1]}", size = 24, font_color = (102, 120, 19))
await img.paste(expImg, (390, 193))
#等级
levelImg = await BuildImage.build_text_image(str(level[0]), size = 32, font_color = (214, 111, 1))
await img.paste(levelImg, (660, 187))
#金币
point = await g_pSqlManager.getUserPointByUid(uid)
pointImg = await BuildImage.build_text_image(str(point), size = 24, font_color = (253, 253, 253))
await img.paste(pointImg, (330, 255))
#点券
bonds = await g_pSqlManager.getUserPointByUid(uid)
bondsImg = await BuildImage.build_text_image("0", size = 24, font_color = (253, 253, 253))
await img.paste(bondsImg, (570, 255))
#清晰度
definition = Config.get_config("zhenxun_plugin_farm", "绘制农场清晰度")
if definition == "medium":
await img.resize(0.6)
elif definition == "hight":
await img.resize(0.8)
elif definition == "original":
pass
else:
await img.resize(0.4)
return img.pic2bytes()
@classmethod
async def drawSoilPlant(cls, uid: str, soilid: str) -> tuple[bool, BuildImage, bool]:
"""绘制植物资源
Args:
uid (str): 用户Uid
soilid (str): 土地id
Returns:
tuple[bool, BuildImage]: [绘制是否成功,资源图片, 是否成熟]
"""
plant = None
soilStatus, soilInfo = await g_pSqlManager.getUserSoilStatusBySoilID(uid, soilid)
if soilStatus == True:
return False, None, False
else:
soilInfo = soilInfo.split(',')
if int(soilInfo[3]) == 4:
plant = BuildImage(background = g_sResourcePath / f"plant/basic/9.png")
await plant.resize(0, 150, 212)
return True, plant, False
plantInfo = g_pJsonManager.m_pPlant['plant'][soilInfo[0]]
currentTime = datetime.now()
matureTime = datetime.fromtimestamp(int(soilInfo[2]))
if currentTime >= matureTime:
phase = int(plantInfo['phase'])
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo[0]}/{phase - 1}.png")
return True, plant, True
else:
plantedTime = datetime.fromtimestamp(int(soilInfo[1]))
elapsedTime = currentTime - plantedTime
elapsedHour = elapsedTime.total_seconds() / 60 / 60
currentStage = int(elapsedHour / (plantInfo['time'] / (plantInfo['phase'] - 1)))
#TODO 缺少判断部分种子是否是通用0阶段图片
if currentStage <= 0:
plant = BuildImage(background = g_sResourcePath / f"plant/basic/0.png")
await plant.resize(0, 35, 58)
else:
plant = BuildImage(background = g_sResourcePath / f"plant/{soilInfo[0]}/{currentStage}.png")
return True, plant, False
@classmethod
async def getUserSeedByUid(cls, uid: str) -> bytes:
"""获取用户种子仓库
Args:
uid (str): 用户Uid
Returns:
bytes: 返回图片
"""
data_list = []
column_name = [
"-",
"种子名称",
"数量",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"再次成熟时间(小时)",
"是否可以上架交易行"
]
seed = await g_pSqlManager.getUserSeedByUid(uid)
if seed == None:
result = await ImageTemplate.table_page(
"种子仓库",
"播种示例:@小真寻 播种 大白菜 [数量]",
column_name,
data_list,
)
return result.pic2bytes()
sell = ""
for item in seed.split(','):
if '|' in item:
seedName, count = item.split('|', 1) #分割一次,避免多竖线问题
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][seedName]
icon = ""
icon_path = g_sResourcePath / f"plant/{seedName}/icon.png"
if icon_path.exists():
icon = (icon_path, 33, 33)
if plantInfo['again'] == True:
sell = "可以"
else:
sell = "不可以"
data_list.append(
[
icon,
seedName,
count,
plantInfo['experience'],
plantInfo['harvest'],
plantInfo['time'],
plantInfo['crop'],
plantInfo['again'],
sell
]
)
except Exception as e:
continue
result = await ImageTemplate.table_page(
"种子仓库",
"播种示例:@小真寻 播种 大白菜 [数量]",
column_name,
data_list,
)
return result.pic2bytes()
@classmethod
async def sowing(cls, uid: str, name: str, num: int = -1) -> str:
"""播种
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int, optional): 播种数量
Returns:
str:
"""
number = 0
count = await g_pSqlManager.getUserSeedByName(uid, name)
if count <= 0:
return f"没有在你的仓库发现{name}种子,快去买点吧!"
#如果播种超过仓库种子
isMax = False
if count < num:
isMax = True
#如果播种全部
isAll = False
if num == -1:
isAll = True
soilNumber = await g_pSqlManager.getUserSoilByUid(uid)
for i in range(1, soilNumber + 1):
if count > 0:
if isAll or num > 0:
soilName = f"soil{i}"
success, message = await g_pSqlManager.getUserSoilStatusBySoilID(uid, soilName)
if success:
#更新种子数量
num -= 1
count -= 1
#记录种子消耗数量
number -= 1
#更新数据库
await g_pSqlManager.updateUserSoilStatusByPlantName(uid, soilName, name)
str = ""
if isMax:
str = f"仓库数量不够那么多,已将剩余数量全部播种!"
elif num > 0:
str = f"播种数量超出开垦土地数量,已将可播种土地成功播种{name}!仓库还剩下{count}个种子"
else:
str = f"播种{name}成功!仓库还剩下{count}个种子"
await g_pSqlManager.addUserSeedByPlant(uid, name, number)
return str
@classmethod
async def harvest(cls, uid: str) -> str:
"""收获作物
Args:
uid (str): 用户Uid
Returns:
str: 返回
"""
isStealingPlant = 0
soilUnlock = await g_pSqlManager.getUserSoilByUid(uid)
soilNames = [f"soil{i + 1}" for i in range(soilUnlock)]
soilStatuses = await asyncio.gather(*[
g_pSqlManager.getUserSoilStatusBySoilID(uid, name)
for name in soilNames
])
harvestRecords: List[str] = []
experience = 0
for (soil_name, (status, info)) in zip(soilNames, soilStatuses):
if len(info) <= 0:
continue
soilInfo = info.split(',')
if int(soilInfo[3]) == 4:
continue
plantId = soilInfo[0]
plantInfo = g_pJsonManager.m_pPlant['plant'][plantId]
currentTime = datetime.now()
matureTime = datetime.fromtimestamp(int(soilInfo[2]))
if currentTime >= matureTime:
number = plantInfo['harvest']
#判断该土地作物是否被透过
if len(soilInfo[4]) > 0:
stealingStatus = soilInfo[4].split('|')
for isUser in stealingStatus:
user = isUser.split('-')
number -= int(user[1])
isStealingPlant += 1
experience += plantInfo['experience']
harvestRecords.append(f"收获作物:{plantId},数量为:{number},经验为:{plantInfo['experience']}")
#更新数据库操作
await g_pSqlManager.addUserPlantByPlant(uid, plantId, number)
await g_pSqlManager.updateUserSoilStatusByPlantName(uid, soil_name, "", 4)
if experience > 0:
harvestRecords.append(f"\t累计获得经验:{experience}")
exp = await g_pSqlManager.getUserExpByUid(uid)
await g_pSqlManager.UpdateUserExpByUid(uid, exp + experience)
if isStealingPlant <= 0:
return "没有可收获的作物哦~ 不要试图拔苗助长"
else:
return "\n".join(harvestRecords)
@classmethod
async def eradicate(cls, uid: str) -> str:
"""铲除作物
TODO 缺少随意铲除作物 目前只能铲除荒废作物
Args:
uid (str): 用户Uid
Returns:
str: 返回
"""
soilUnlock = await g_pSqlManager.getUserSoilByUid(uid)
soilNames = [f"soil{i + 1}" for i in range(soilUnlock)]
soilStatuses = await asyncio.gather(*[
g_pSqlManager.getUserSoilStatusBySoilID(uid, name)
for name in soilNames
])
experience = 0
for (soil_name, (status, info)) in zip(soilNames, soilStatuses):
if info:
soilInfo = info.split(',')
if int(soilInfo[3]) == 4:
experience += 3
#批量更新数据库操作
await g_pSqlManager.updateUserSoilStatusByPlantName(uid, soil_name, "", 0)
if experience > 0:
exp = await g_pSqlManager.getUserExpByUid(uid)
await g_pSqlManager.UpdateUserExpByUid(uid, exp + experience)
return f"成功铲除荒废作物,累计获得经验:{experience}"
else:
return "没有可以铲除的作物"
@classmethod
async def getUserPlantByUid(cls, uid: str) -> bytes:
"""获取用户作物仓库
Args:
uid (str): 用户Uid
Returns:
bytes: 返回图片
"""
data_list = []
column_name = [
"-",
"作物名称",
"数量",
"单价",
"总价",
"是否可以上架交易行"
]
plant = await g_pSqlManager.getUserPlantByUid(uid)
if plant == None:
result = await ImageTemplate.table_page(
"作物仓库",
"播种示例:@小真寻 出售作物 大白菜 [数量]",
column_name,
data_list,
)
return result.pic2bytes()
sell = ""
for item in plant.split(','):
if '|' in item:
plantName, count = item.split('|', 1) #分割一次,避免多竖线问题
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][plantName]
icon = ""
icon_path = g_sResourcePath / f"plant/{plantName}/icon.png"
if icon_path.exists():
icon = (icon_path, 33, 33)
if plantInfo['again'] == True:
sell = "可以"
else:
sell = "不可以"
number = int(count) * plantInfo['price']
data_list.append(
[
icon,
plantName,
count,
plantInfo['price'],
number,
sell
]
)
except Exception as e:
continue
result = await ImageTemplate.table_page(
"作物仓库",
"播种示例:@小真寻 出售作物 大白菜 [数量]",
column_name,
data_list,
)
return result.pic2bytes()
@classmethod
async def stealing(cls, uid: str, target: str) -> str:
"""偷菜
Args:
uid (str): 用户Uid
target (str): 被偷用户Uid
Returns:
str: 返回
"""
#用户信息
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
#用户可偷次数
userStealing = userInfo["stealing"].split('|')
if userStealing[0] == '':
userStealing[0] = date.today().strftime('%Y-%m-%d')
userStealing.append(5)
elif date.fromisoformat(userStealing[0]) != date.today():
userStealing[0] = date.today().strftime('%Y-%m-%d')
userStealing[1] = 5
if int(userStealing[1]) <= 0:
return "你今天可偷次数到达上限啦,手下留情吧"
#获取用户解锁地块数量
soilUnlockNum = int(userInfo["soil"])
plant = {}
#根据解锁土地,获取每块土地状态信息
soilNames = [f"soil{i + 1}" for i in range(soilUnlockNum)]
soilStatuses = await asyncio.gather(*[
g_pSqlManager.getUserSoilStatusBySoilID(target, name)
for name in soilNames
])
isStealing = False
harvestRecords: List[str] = []
isStealingNumber = 0
isStealingPlant = 0
for (soilName, (status, info)) in zip(soilNames, soilStatuses):
isStealing = False
if not info:
continue
soilInfo = info.split(',')
if int(soilInfo[3]) == 4:
continue
#作物ID
plantId = soilInfo[0]
#作物信息
plantInfo = g_pJsonManager.m_pPlant['plant'][plantId]
currentTime = datetime.now()
matureTime = datetime.fromtimestamp(int(soilInfo[2]))
#偷窃状态
stealingStatus: list[str] = []
#偷窃数量
stealingNumber = 0
if currentTime >= matureTime:
if soilInfo[4]:
#先获取用户是否偷过该土地
stealingStatus = soilInfo[4].split('|')
for isUser in stealingStatus:
user = isUser.split('-')
if user[0] == uid:
isStealing = True
isStealingNumber += 1
break
stealingNumber -= int(user[1])
#如果偷过,则跳过该土地
if isStealing:
continue
stealingNumber += plantInfo['harvest']
randomNumber = random.choice([1, 2])
randomNumber = min(randomNumber, stealingNumber)
logger.info(f"{randomNumber}")
if randomNumber > 0:
await g_pSqlManager.addUserPlantByPlant(uid, plantId, randomNumber)
harvestRecords.append(f"成功偷到作物:{plantId},数量为:{randomNumber}")
stealingStatus.append(f"{uid}-{randomNumber}")
isStealingPlant += 1
#如果将作物偷完,就直接更新状态 并记录用户偷取过
if plantInfo['harvest'] - randomNumber + stealingNumber == 0:
sql = f"UPDATE soil SET {soilName} = ',,,4,{'|'.join(stealingStatus)}' WHERE uid = '{target}'"
else:
sql = f"UPDATE soil SET {soilName} = '{soilInfo[0]},{soilInfo[1]},{soilInfo[2]},{soilInfo[3]},{'|'.join(stealingStatus)}' WHERE uid = '{target}'"
await g_pSqlManager.executeDB(sql)
if isStealingPlant <= 0 and isStealingNumber <= 0:
return "目标没有作物可以被偷"
elif isStealingPlant <= 0 and isStealingNumber > 0:
return "你已经偷过目标啦,请手下留情"
else:
userStealing[1] = int(userStealing[1]) - 1
# 转换所有元素为字符串
userStealing_str = [str(item) for item in userStealing]
sql = f"UPDATE user SET stealing = '{userStealing[0]}|{userStealing[1]}' WHERE uid = {uid}"
#更新用户每日偷取次数
await g_pSqlManager.executeDB(sql)
return "\n".join(harvestRecords)
@classmethod
async def reclamationCondition(cls, uid: str) -> str:
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
rec = g_pJsonManager.m_pLevel["reclamation"]
try:
if userInfo['soil'] >= 30:
return "你已经开垦了全部土地"
rec = rec[f"{userInfo['soil'] + 1}"]
level = rec['level']
point = rec['point']
item = rec['item']
str = ""
if len(item) == 0:
str = f"下次升级所需条件:等级:{level},农场币:{point}"
else:
str = f"下次升级所需条件:等级:{level},农场币:{point},物品:{item}"
return str
except Exception as e:
return "获取升级土地条件失败!"
@classmethod
async def reclamation(cls, uid: str) -> str:
userInfo = await g_pSqlManager.getUserInfoByUid(uid)
level = await g_pSqlManager.getUserLevelByUid(uid)
rec = g_pJsonManager.m_pLevel["reclamation"]
try:
if userInfo['soil'] >= 30:
return "你已经开垦了全部土地"
rec = rec[f"{userInfo['soil'] + 1}"]
levelFileter = rec['level']
point = rec['point']
item = rec['item']
logger.info(f"{level[0]}")
if level[0] < levelFileter:
return f"当前用户等级{level[0]},升级所需等级为{levelFileter}"
if userInfo['point'] < point:
return f"当前用户农场币不足,升级所需农场币为{point}"
#TODO 缺少判断需要的Item
sql = f"UPDATE user SET point = '{userInfo['point'] - point}', soil = '{userInfo['soil'] + 1}' WHERE uid = {uid}"
await g_pSqlManager.executeDB(sql)
return "开垦土地成功!"
except Exception as e:
return "执行开垦失败!"
g_pFarmManager = CFarmManager()
-209
View File
@@ -1,209 +0,0 @@
import math
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.image_utils import ImageTemplate
from ..config import g_pJsonManager, g_sResourcePath
from ..database import g_pSqlManager
class CShopManager:
@classmethod
async def getSeedShopImage(cls, num: int = 1) -> bytes:
"""获取商店页面
Returns:
bytes: 返回商店图片bytes
"""
data_list = []
column_name = [
"-",
"种子名称",
"解锁等级",
"种子单价",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"再次成熟时间(小时)",
"是否可以上架交易行"
]
sell = ""
plants = list(g_pJsonManager.m_pPlant['plant'].items())
start = (num - 1) * 15
maxItems = min(len(plants) - start, 15)
items = plants[start:start + maxItems]
for key, plant in items:
icon = ""
icon_path = g_sResourcePath / f"plant/{key}/icon.png"
if icon_path.exists():
icon = (icon_path, 33, 33)
if plant['again'] == True:
sell = "可以"
else:
sell = "不可以"
data_list.append(
[
icon,
key,
plant['level'],
plant['price'],
plant['experience'],
plant['harvest'],
plant['time'],
plant['crop'],
plant['again'],
sell
]
)
count = math.ceil(len(g_pJsonManager.m_pPlant['plant']) / 15)
title = f"种子商店 页数: {num}/{count}"
result = await ImageTemplate.table_page(
title,
"购买示例:@小真寻 购买种子 大白菜 5",
column_name,
data_list,
)
return result.pic2bytes()
@classmethod
async def buySeed(cls, uid: str, name: str, num: int = 1) -> str:
"""购买种子
Args:
uid (str): 用户Uid
name (str): 植物名称
num (int, optional): 购买数量
Returns:
str:
"""
if num <= 0:
return "请输入购买数量!"
plantInfo = None
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][name]
except Exception as e:
return "购买出错!请检查需购买的种子名称!"
level = await g_pSqlManager.getUserLevelByUid(uid)
if level[0] < int(plantInfo['level']):
return "你的等级不够哦,努努力吧"
point = await g_pSqlManager.getUserPointByUid(uid)
total = int(plantInfo['price']) * num
logger.debug(f"用户:{uid}购买{name},数量为{num}。用户农场币为{point},购买需要{total}")
if point < total:
return "你的农场币不够哦~ 快速速氪金吧!"
else:
await g_pSqlManager.updateUserPointByUid(uid, point - total)
if await g_pSqlManager.addUserSeedByPlant(uid, name, num) == False:
return "购买失败,执行数据库错误!"
return f"成功购买{name},花费{total}农场币, 剩余{point - total}农场币"
@classmethod
async def sellPlantByUid(cls, uid: str, name: str = "", num: int = 1) -> str:
"""出售作物
Args:
uid (str): 用户Uid
Returns:
str:
"""
plant = await g_pSqlManager.getUserPlantByUid(uid)
if plant == None:
return "你仓库没有可以出售的作物"
point = 0
totalSold = 0
remainingItems = []
isAll = False
if num == -1:
isAll = True
items = plant.split(',')
if len(name) <= 0:
#出售全部
for item in items:
if '|' in item:
plant_name, count_str = item.split('|', 1)
try:
count = int(count_str)
plant_info = g_pJsonManager.m_pPlant['plant'][plant_name]
point += plant_info['price'] * count
except Exception:
continue
await g_pSqlManager.updateUserPlantByUid(uid, "") # 清空仓库
else:
for item in items:
if '|' in item:
plantName, countStr = item.split('|', 1)
try:
count = int(countStr)
if plantName == name:
if isAll:
sellAmount = count
else:
sellAmount = min(num, count)
totalSold += sellAmount
remaining = count - sellAmount
if remaining > 0:
remainingItems.append(f"{plantName}|{remaining}")
if isAll == False:
num -= sellAmount
break
except (ValueError, TypeError):
continue
if num > 0 and isAll == False:
return f"出售作物{name}出错:数量不足"
#计算收益
try:
plantInfo = g_pJsonManager.m_pPlant['plant'][name]
totalPoint = plantInfo['price'] * totalSold
except KeyError:
return f"出售作物{name}出错:作物不存在"
#更新剩余作物
remainingPlant = ','.join(remainingItems) if remainingItems else ""
await g_pSqlManager.updateUserPlantByUid(uid, remainingPlant)
#更新农场币
p = await g_pSqlManager.getUserPointByUid(uid)
await g_pSqlManager.updateUserPointByUid(uid, p + totalPoint)
if name:
return f"成功出售{name},获得农场币:{totalPoint}"
else:
return f"成功出售所有作物,获得农场币:{totalPoint}"
g_pShopManager = CShopManager()
+71
View File
@@ -0,0 +1,71 @@
# 真寻农场更新日志
## V1.5
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## V1.4
用户方面
---
- 新增种子商店筛选功能(如果没有BUG的话后续我的种子、我的作物等也会加入筛选功能
- 新增签到功能(测试
- 新增农场详述功能,能通过该功能更加详细的观看农场数据
- 修复了迁移旧数据库无法正常迁移的BUG
- 修复了偷菜会导致偷自己的BUG
- 修正了作物阶段绘制不正确的BUG
代码方面
---
- 修正了多阶段作物成长素材计算逻辑
- 修正了作物数据库字段错乱的问题
- 作物新增offset字段,用于以后偏移坐标和大小(尚未启用,该模式有商议
## V1.3
用户方面
---
- 新增部分作物
- 修复一个严重BUG, 该BUG会导致无法重复偷别人菜的问题
代码方面
---
- 将作物数据从plant.json迁移至resource/db/plant.db中,并将操作json改为操作数据库
## V1.2
用户方面
---
- 修复重大BUG,该BUG会导致用户经验等级计算异常
- 修正种子价格,现在不会以单价的形式购买种子了,而是新增的种子价格
代码方面
---
- 彻底重构数据库,但是不会像V1.1一样导致用户数据丢失
- 迁移部分代码结构,使文件名和代码功能更加匹配
- 加入事件机制,采用类似Qt信号槽机制
## V1.1
用户方面
---
- 完善我的农场图片资源,现在会在左上角显示经验、等级、金币等详细信息了
- 完善出售作物逻辑,现在可以空置作物名称来一键出售全部作物了,也可以选择空置数量来一键出售仓库种指定作物
- 完善播种逻辑,现在可以空置数量来一键播种指定作物了
- 新增更改农场名指令
- 改进对土地开垦条件判断
代码方面
---
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法,但是会导致V1.0用户的作物和种子丢失
## V1.0
世界的起源。
-43
View File
@@ -1,43 +0,0 @@
from unittest import result
import requests
class CRequestManager:
@classmethod
async def post(cls, url, json_data=None, form_data=None):
"""发送 POST 请求(支持 JSON/Form-Data 格式)"""
try:
headers = {"Content-Type": "application/json"} if json_data else None
response = requests.post(
url,
json=json_data,
data=form_data,
headers=headers,
timeout=5
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
return {}
@classmethod
async def sign(cls, uid: str) -> str:
a = await cls.post("http://diuse.work:9099/testPost", json_data={"level":3})
result = ""
type = int(a["type"])
if type == 1:
result = f"签到成功 type = 1"
elif type == 2:
result = f"签到成功 type = 2"
else:
result = f"签到成功 type = {type}"
return result
g_pRequestManager = CRequestManager()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"version": 0.41}
+777
View File
@@ -0,0 +1,777 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能功能展示 - 参数可视化</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "{{ font_family }}", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
}
@font-face {
font-family: "{{ font_family }}";
src: url("../font/Rounded.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
body {
background: linear-gradient(135deg, #fff9f9 0%, #f0f9ff 100%);
color: #5a5a5a;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 40px;
padding: 30px 20px;
}
h1 {
color: #ff85a2;
font-size: 3rem;
margin-bottom: 15px;
text-shadow: 2px 2px 0px rgba(255, 133, 162, 0.2);
position: relative;
display: inline-block;
}
h1::after {
content: "";
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 4px;
background: linear-gradient(90deg, #ff85a2, #a2d2ff);
border-radius: 2px;
}
.description {
font-size: 1.2rem;
color: #888;
max-width: 800px;
margin: 20px auto;
line-height: 1.6;
background-color: rgba(255, 255, 255, 0.7);
padding: 20px;
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.legend {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
background: white;
padding: 8px 15px;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
font-size: 0.9rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
margin-right: 8px;
}
.required-color {
background-color: #ff6b9c;
}
.optional-color {
background-color: #a2d2ff;
}
.conditional-color {
background-color: #b9fbc0;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 30px;
margin-top: 40px;
}
.feature-box {
background: linear-gradient(135deg, #ffffff 0%, #f8f8f8 100%);
border-radius: 25px;
padding: 25px;
box-shadow: 0 8px 20px rgba(255, 133, 162, 0.15);
transition: all 0.3s ease;
border: 3px solid transparent;
position: relative;
overflow: hidden;
cursor: pointer;
}
.feature-box:hover {
transform: translateY(-5px);
box-shadow: 0 12px 25px rgba(255, 133, 162, 0.25);
border-color: #ffc2d1;
}
.feature-box::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(90deg, #ff85a2, #a2d2ff);
}
.feature-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.feature-icon {
font-size: 2.5rem;
margin-right: 15px;
}
.feature-name {
font-size: 1.6rem;
font-weight: bold;
color: #ff6b9c;
}
.parameters-section {
margin: 20px 0;
}
.section-title {
font-size: 1rem;
color: #888;
margin-bottom: 10px;
display: flex;
align-items: center;
}
.section-title::before {
content: "📋";
margin-right: 8px;
}
.parameters-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.parameter-item {
display: flex;
align-items: center;
padding: 10px 15px;
border-radius: 12px;
font-size: 0.95rem;
transition: all 0.2s ease;
}
.parameter-item:hover {
transform: translateX(5px);
}
.required-param {
background-color: rgba(255, 107, 156, 0.1);
border-left: 4px solid #ff6b9c;
}
.optional-param {
background-color: rgba(162, 210, 255, 0.1);
border-left: 4px solid #a2d2ff;
}
.conditional-param {
background-color: rgba(185, 251, 192, 0.1);
border-left: 4px solid #b9fbc0;
}
.param-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: bold;
margin-right: 12px;
min-width: 60px;
text-align: center;
}
.required-badge {
background-color: #ff6b9c;
color: white;
}
.optional-badge {
background-color: #a2d2ff;
color: white;
}
.conditional-badge {
background-color: #b9fbc0;
color: #333;
}
.param-description {
flex: 1;
}
.logic-section {
margin-top: 20px;
padding: 15px;
background-color: rgba(255, 213, 165, 0.1);
border-radius: 12px;
border-left: 4px solid #ffd6a5;
}
.logic-title {
font-weight: bold;
color: #ff9e6d;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.logic-title::before {
content: "🔍";
margin-right: 8px;
}
.logic-content {
font-size: 0.9rem;
color: #666;
line-height: 1.5;
}
.usage-example {
margin-top: 15px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: #333;
border-left: 3px solid #888;
}
.decoration {
position: absolute;
width: 80px;
height: 80px;
opacity: 0.1;
z-index: 0;
}
.decoration-1 {
top: -20px;
left: -20px;
background-color: #ff85a2;
border-radius: 50%;
}
.decoration-2 {
bottom: -20px;
right: -20px;
background-color: #a2d2ff;
border-radius: 50%;
}
@media (max-width: 768px) {
.features-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 2.2rem;
}
}
footer {
margin-top: 50px;
text-align: center;
color: #aaa;
font-size: 0.9rem;
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>✨ 真寻农场帮助菜单 ✨</h1>
<p class="description">简单介绍一下农场的各个功能和食用方法</p>
<div class="legend">
<div class="legend-item">
<div class="legend-color required-color"></div>
<span>必填参数</span>
</div>
<div class="legend-item">
<div class="legend-color optional-color"></div>
<span>可选参数</span>
</div>
<div class="legend-item">
<div class="legend-color conditional-color"></div>
<span>条件参数</span>
</div>
</div>
</header>
<main>
<div class="features-grid">
<!-- 必填 + 可选参数 -->
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">at 开通农场</div>
</div>
<div class="logic-section">
<div class="logic-title">条件逻辑</div>
<div class="logic-content">
• 需要at小真寻<br>
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">我的农场</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">农场详述</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">我的农场币</div>
</div>
</div>
<!-- 条件参数 -->
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">种子商店</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item conditional-param">
<span class="param-badge conditional-badge">条件</span>
<span class="param-description">参数 - 根据类型决定含义</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">条件逻辑</div>
<div class="logic-content">
• 如果参数是中文 → 进入筛选模式,可接页码参数<br>
• 如果参数是数字 → 直接作为页码使用<br>
</div>
</div>
<div class="usage-example">
使用示例1: 种子商店 胡萝<br>
使用示例2: 种子商店 2<br>
使用示例3: 种子商店 胡萝 3
</div>
</div>
<!-- 多个参数 -->
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">购买种子</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item required-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">作物/种子名称</span>
</div>
<div class="parameter-item optional-param">
<span class="param-badge optional-badge">可选</span>
<span class="param-description">数量</span>
</div>
</div>
</div>
<div class="usage-example">
使用示例: 购买种子 胡萝卜
使用示例: 购买种子 胡萝卜 5
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">我的种子</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">播种</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item conditional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">作物/种子名称</span>
</div>
<div class="parameter-item optional-param">
<span class="param-badge optional-badge">可选</span>
<span class="param-description">数量</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 数量不填默认将最大可能播种
</div>
</div>
<div class="usage-example">
使用示例1: 种子商店 胡萝<br>
使用示例2: 种子商店 2<br>
使用示例3: 种子商店 胡萝 3
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">收获</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">铲除</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">我的作物</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">出售作物</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item conditional-param">
<span class="param-badge optional-badge">可选</span>
<span class="param-description">作物/种子名称</span>
</div>
<div class="parameter-item optional-param">
<span class="param-badge optional-badge">可选</span>
<span class="param-description">数量</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 不填写作物名将售卖仓库种全部作物<br>
• 填作物名不填数量将指定作物全部出售
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">偷菜 at</div>
</div>
<div class="logic-section">
<div class="logic-title">条件逻辑</div>
<div class="logic-content">
• 每人每天只能偷5次
• 后续需要at目标且目标开通真寻农场
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">开垦</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">购买农场币</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">数量</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 数量为消耗金币的数量
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">更改农场名</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">新的农场名</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 仅支持部分特殊符号
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">农场签到</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">土地升级</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">地块ID</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 地块ID通过农场详述获取
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">作物加锁</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">地块ID</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 地块ID通过农场详述获取
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">作物解锁</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">地块ID</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 地块ID通过农场详述获取
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">我的点券</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">地块ID</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 地块ID通过农场详述获取
</div>
</div>
</div>
<div class="feature-box">
<div class="decoration decoration-1"></div>
<div class="decoration decoration-2"></div>
<div class="feature-header">
<div class="feature-name">点券兑换</div>
</div>
<div class="parameters-section">
<div class="section-title">参数列表</div>
<div class="parameters-list">
<div class="parameter-item optional-param">
<span class="param-badge required-badge">必填</span>
<span class="param-description">地块ID</span>
</div>
</div>
</div>
<div class="logic-section">
<div class="logic-title">操作提示</div>
<div class="logic-content">
• 地块ID通过农场详述获取
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More