mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-31 15:00:03 +08:00
add # 新增开垦、升级土地功能
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
# 土地配置与地块顺序
|
||||||
|
|
||||||
|
`src/farm_core/data/land_rules.json` 按用户提供的 `91.png` 逐行录入。
|
||||||
|
这是用户选定的参考规则,不标记为已验证的现行官方全量配置。
|
||||||
|
|
||||||
|
- 开垦保留图片序号 7~24 的付费条件;红、黑、金土地各保留 1~24。
|
||||||
|
- 图片金币单位为万,配置中的 `coin_cost` 为乘 10000 后的整数。
|
||||||
|
- 前 6 块由 `initial_plot_count: 6` 表示,不重复保存空条件记录。
|
||||||
|
新用户默认 6 块空地是用户另行明确指定的规则。已有账号不自动补地。
|
||||||
|
- `ordinal` 是图片表内的条件序号,不等同于实际地块 `slot`。
|
||||||
|
后续升级服务必须明确这两者的对应方式,不能混用。
|
||||||
|
- 表外序号,以及材料、星钻替代支付、冷却和额外解锁条件没有在图中
|
||||||
|
给出。不能默认免费、默认通过,或套用最后一行。24 不是土地总上限。
|
||||||
|
- 本次提供规则数据;开垦和土地升级指令尚未接入这份配置。
|
||||||
|
|
||||||
|
## 新用户注册
|
||||||
|
|
||||||
|
注册事务创建 1~6 号空地,初始农场币仍为 500。重复注册不额外发地。
|
||||||
|
不修改、补齐或重编号已有数据库中的地块记录。
|
||||||
|
|
||||||
|
## 绘图编号顺序
|
||||||
|
|
||||||
|
沿原有五条斜排的坐标方向,前两排交错推进,后续逐排推进:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1 3 5 7 9 11
|
||||||
|
2 4 6 8 10 12
|
||||||
|
13 14 15 16 17 18
|
||||||
|
19 20 21 22 23 24
|
||||||
|
25 26 27 28 29 30
|
||||||
|
```
|
||||||
|
|
||||||
|
前 6 块地形成两排、每排 3 块;前 12 块完成后,13 号起按单排推进。
|
||||||
|
`metadata.json` 的 `positions[slot - 1]` 是统一的绘图映射,土地、作物、
|
||||||
|
虫子和详情中的地块标注都使用它。地形坐标不变,只重排前 12 项。
|
||||||
|
已有账号的同一 `slot` 保留原作物和资产,展示位置随此映射调整。
|
||||||
|
|
||||||
|
## 用户查询入口
|
||||||
|
|
||||||
|
真寻接入新增「地块编号」,支持「地块编号 高清」。它复用正式渲染器,
|
||||||
|
按当前账号快照标注全部 30 个位置:浅色编号为已开垦,绿色为待开垦。
|
||||||
|
图片附带编号顺序和「农场详述」入口说明,并保留当前季节、作物及头像。
|
||||||
|
该查询无需消息写入标识,不更改作物、土地或资产。缺少字体时返回文字清单。
|
||||||
|
|
||||||
|
「农场帮助」包含该入口;首次注册成功回复显示本次实际初始地块数并提示
|
||||||
|
发送「地块编号」。重复注册只提示查询入口,不声称再次获得土地。
|
||||||
|
日常「我的农场」仍不显示编号,编号图与普通图使用不同的成图缓存键。
|
||||||
@@ -77,7 +77,7 @@ def handler_for(action):
|
|||||||
message_id = getattr(event, "id", None)
|
message_id = getattr(event, "id", None)
|
||||||
try:
|
try:
|
||||||
avatar = (await avatar_for(user_id, platform)
|
avatar = (await avatar_for(user_id, platform)
|
||||||
if action in ("farm", "details") else None)
|
if action in ("farm", "details", "plot_ids") else None)
|
||||||
response = await runtime.execute(
|
response = await runtime.execute(
|
||||||
action, args.extract_plain_text(), bot_id=str(bot.self_id),
|
action, args.extract_plain_text(), bot_id=str(bot.self_id),
|
||||||
platform=platform, user_id=user_id, message_id=message_id,
|
platform=platform, user_id=user_id, message_id=message_id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""真寻接入编排;核心私有导入,测试数据与旧版隔离。"""
|
"""真寻接入编排;核心私有导入,测试数据与旧版隔离。"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from dataclasses import replace
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -9,7 +10,9 @@ import shlex
|
|||||||
from ._vendor.farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
from ._vendor.farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
||||||
from ._vendor.farm_core.domain import FarmError
|
from ._vendor.farm_core.domain import FarmError
|
||||||
from ._vendor.farm_core.storage import SCHEMA_VERSION
|
from ._vendor.farm_core.storage import SCHEMA_VERSION
|
||||||
from ._vendor.farm_core.presentation import receipt_text, snapshot_text
|
from ._vendor.farm_core.presentation import (
|
||||||
|
plot_ids_text, receipt_text, snapshot_text,
|
||||||
|
)
|
||||||
from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
|
from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
|
||||||
from ._vendor.farm_render.seasons import season_for
|
from ._vendor.farm_render.seasons import season_for
|
||||||
from ._vendor.farm_render.shop import shop_page
|
from ._vendor.farm_render.shop import shop_page
|
||||||
@@ -23,10 +26,11 @@ COMMANDS = {
|
|||||||
"种子商店":"shop", "农场帮助":"help", "购买种子":"buy",
|
"种子商店":"shop", "农场帮助":"help", "购买种子":"buy",
|
||||||
"播种":"sow", "收获":"harvest", "铲除":"eradicate",
|
"播种":"sow", "收获":"harvest", "铲除":"eradicate",
|
||||||
"作物加锁":"lock", "作物解锁":"unlock", "出售作物":"sell",
|
"作物加锁":"lock", "作物解锁":"unlock", "出售作物":"sell",
|
||||||
"农场除虫":"clear_pests", "除虫":"clear_pests",
|
"农场除虫":"clear_pests", "除虫":"clear_pests", "地块编号":"plot_ids",
|
||||||
}
|
}
|
||||||
HELP = (
|
HELP = (
|
||||||
"开通农场 [名称]|我的农场 [高清]|农场详述 [页码]\n"
|
"开通农场 [名称]|我的农场 [高清]|农场详述 [页码]\n"
|
||||||
|
"地块编号 [高清]|查看当前土地编号及开垦位置\n"
|
||||||
"种子商店 [页码]|购买种子 作物 数量|播种 作物 [数量]\n"
|
"种子商店 [页码]|购买种子 作物 数量|播种 作物 [数量]\n"
|
||||||
"收获|铲除|农场除虫|我的种子|我的作物\n"
|
"收获|铲除|农场除虫|我的种子|我的作物\n"
|
||||||
"作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n"
|
"作物加锁 作物|作物解锁 作物|出售作物 [作物] [数量]\n"
|
||||||
@@ -232,10 +236,12 @@ class Runtime:
|
|||||||
if args:
|
if args:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
return snapshot_text(await self.service.snapshot(who))
|
return snapshot_text(await self.service.snapshot(who))
|
||||||
if action == "farm":
|
if action in ("farm", "plot_ids"):
|
||||||
if args not in ([], ["高清"]):
|
if args not in ([], ["高清"]):
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
if not self.rendering:
|
if not self.rendering:
|
||||||
|
if action == "plot_ids":
|
||||||
|
return plot_ids_text(await self.service.snapshot(who))
|
||||||
return "请配置 FARM_PREVIEW_FONT 为可用中文字体路径。"
|
return "请配置 FARM_PREVIEW_FONT 为可用中文字体路径。"
|
||||||
now = self.service.clock()
|
now = self.service.clock()
|
||||||
theme = season_for(now)
|
theme = season_for(now)
|
||||||
@@ -245,6 +251,8 @@ class Runtime:
|
|||||||
if args else RenderOptions(format=self.image_format,
|
if args else RenderOptions(format=self.image_format,
|
||||||
theme=theme)
|
theme=theme)
|
||||||
)
|
)
|
||||||
|
if action == "plot_ids":
|
||||||
|
options = replace(options, show_plot_ids=True)
|
||||||
picture = await self.rendering.my_farm(
|
picture = await self.rendering.my_farm(
|
||||||
self.service, who, options=options, avatar=avatar,
|
self.service, who, options=options, avatar=avatar,
|
||||||
supported=("JPEG",),
|
supported=("JPEG",),
|
||||||
@@ -280,7 +288,16 @@ class Runtime:
|
|||||||
receipt = await getattr(self.service, action)(who, op)
|
receipt = await getattr(self.service, action)(who, op)
|
||||||
else:
|
else:
|
||||||
return HELP
|
return HELP
|
||||||
return receipt_text(receipt)
|
text = receipt_text(receipt)
|
||||||
|
if action == 'register':
|
||||||
|
if receipt.data['created']:
|
||||||
|
count = receipt.data.get('plot_count')
|
||||||
|
granted = (f'已获得{count}块土地' if count is not None
|
||||||
|
else '已获得初始土地')
|
||||||
|
text += f'\n{granted},发送「地块编号」查看位置。'
|
||||||
|
else:
|
||||||
|
text += '\n发送「地块编号」查看土地位置。'
|
||||||
|
return text
|
||||||
except FarmError as error:
|
except FarmError as error:
|
||||||
return ERRORS.get(error.code, "操作未完成:" + error.code)
|
return ERRORS.get(error.code, "操作未完成:" + error.code)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ class FarmService:
|
|||||||
|
|
||||||
def operation(repository, now):
|
def operation(repository, now):
|
||||||
created = repository.register(name)
|
created = repository.register(name)
|
||||||
return self._result(repository, created=created)
|
return self._result(
|
||||||
|
repository, created=created,
|
||||||
|
plot_count=len(repository.plots()),
|
||||||
|
)
|
||||||
|
|
||||||
return await self._execute(
|
return await self._execute(
|
||||||
identity, operation_id, "register", {"name": name}, operation,
|
identity, operation_id, "register", {"name": name}, operation,
|
||||||
|
|||||||
@@ -0,0 +1,480 @@
|
|||||||
|
{
|
||||||
|
"initial_plot_count": 6,
|
||||||
|
"expansion": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"required_level": 5,
|
||||||
|
"coin_cost": 10000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"required_level": 7,
|
||||||
|
"coin_cost": 20000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"required_level": 9,
|
||||||
|
"coin_cost": 30000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"required_level": 11,
|
||||||
|
"coin_cost": 50000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"required_level": 13,
|
||||||
|
"coin_cost": 70000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"required_level": 15,
|
||||||
|
"coin_cost": 90000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 13,
|
||||||
|
"required_level": 17,
|
||||||
|
"coin_cost": 120000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 14,
|
||||||
|
"required_level": 19,
|
||||||
|
"coin_cost": 150000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 15,
|
||||||
|
"required_level": 21,
|
||||||
|
"coin_cost": 180000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 16,
|
||||||
|
"required_level": 23,
|
||||||
|
"coin_cost": 230000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 17,
|
||||||
|
"required_level": 25,
|
||||||
|
"coin_cost": 300000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 18,
|
||||||
|
"required_level": 27,
|
||||||
|
"coin_cost": 500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 19,
|
||||||
|
"required_level": 29,
|
||||||
|
"coin_cost": 850000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 20,
|
||||||
|
"required_level": 31,
|
||||||
|
"coin_cost": 1100000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 21,
|
||||||
|
"required_level": 33,
|
||||||
|
"coin_cost": 1300000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 22,
|
||||||
|
"required_level": 35,
|
||||||
|
"coin_cost": 1500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"required_level": 37,
|
||||||
|
"coin_cost": 1700000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 24,
|
||||||
|
"required_level": 39,
|
||||||
|
"coin_cost": 2000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upgrades": [
|
||||||
|
{
|
||||||
|
"land_type": "red",
|
||||||
|
"name": "红土地",
|
||||||
|
"from_land_type": "normal",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"required_level": 28,
|
||||||
|
"coin_cost": 200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"required_level": 29,
|
||||||
|
"coin_cost": 220000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"required_level": 30,
|
||||||
|
"coin_cost": 240000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"required_level": 31,
|
||||||
|
"coin_cost": 260000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"required_level": 32,
|
||||||
|
"coin_cost": 290000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"required_level": 33,
|
||||||
|
"coin_cost": 320000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"required_level": 34,
|
||||||
|
"coin_cost": 350000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"required_level": 35,
|
||||||
|
"coin_cost": 380000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"required_level": 36,
|
||||||
|
"coin_cost": 410000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"required_level": 37,
|
||||||
|
"coin_cost": 440000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"required_level": 38,
|
||||||
|
"coin_cost": 480000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"required_level": 39,
|
||||||
|
"coin_cost": 520000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 13,
|
||||||
|
"required_level": 40,
|
||||||
|
"coin_cost": 560000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 14,
|
||||||
|
"required_level": 41,
|
||||||
|
"coin_cost": 600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 15,
|
||||||
|
"required_level": 42,
|
||||||
|
"coin_cost": 650000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 16,
|
||||||
|
"required_level": 43,
|
||||||
|
"coin_cost": 700000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 17,
|
||||||
|
"required_level": 44,
|
||||||
|
"coin_cost": 770000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 18,
|
||||||
|
"required_level": 45,
|
||||||
|
"coin_cost": 900000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 19,
|
||||||
|
"required_level": 47,
|
||||||
|
"coin_cost": 1500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 20,
|
||||||
|
"required_level": 49,
|
||||||
|
"coin_cost": 2000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 21,
|
||||||
|
"required_level": 51,
|
||||||
|
"coin_cost": 3000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 22,
|
||||||
|
"required_level": 53,
|
||||||
|
"coin_cost": 4000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"required_level": 55,
|
||||||
|
"coin_cost": 5500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 24,
|
||||||
|
"required_level": 57,
|
||||||
|
"coin_cost": 6800000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"land_type": "black",
|
||||||
|
"name": "黑土地",
|
||||||
|
"from_land_type": "red",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"required_level": 40,
|
||||||
|
"coin_cost": 500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"required_level": 41,
|
||||||
|
"coin_cost": 600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"required_level": 42,
|
||||||
|
"coin_cost": 700000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"required_level": 43,
|
||||||
|
"coin_cost": 800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"required_level": 44,
|
||||||
|
"coin_cost": 900000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"required_level": 45,
|
||||||
|
"coin_cost": 1000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"required_level": 46,
|
||||||
|
"coin_cost": 2000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"required_level": 47,
|
||||||
|
"coin_cost": 2200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"required_level": 48,
|
||||||
|
"coin_cost": 2400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"required_level": 49,
|
||||||
|
"coin_cost": 2600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"required_level": 50,
|
||||||
|
"coin_cost": 2800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"required_level": 51,
|
||||||
|
"coin_cost": 3000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 13,
|
||||||
|
"required_level": 52,
|
||||||
|
"coin_cost": 4000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 14,
|
||||||
|
"required_level": 53,
|
||||||
|
"coin_cost": 4200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 15,
|
||||||
|
"required_level": 54,
|
||||||
|
"coin_cost": 4400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 16,
|
||||||
|
"required_level": 55,
|
||||||
|
"coin_cost": 4600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 17,
|
||||||
|
"required_level": 56,
|
||||||
|
"coin_cost": 4800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 18,
|
||||||
|
"required_level": 57,
|
||||||
|
"coin_cost": 5000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 19,
|
||||||
|
"required_level": 59,
|
||||||
|
"coin_cost": 6000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 20,
|
||||||
|
"required_level": 61,
|
||||||
|
"coin_cost": 6200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 21,
|
||||||
|
"required_level": 63,
|
||||||
|
"coin_cost": 6400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 22,
|
||||||
|
"required_level": 65,
|
||||||
|
"coin_cost": 6600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"required_level": 67,
|
||||||
|
"coin_cost": 6800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 24,
|
||||||
|
"required_level": 69,
|
||||||
|
"coin_cost": 7000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"land_type": "gold",
|
||||||
|
"name": "金土地",
|
||||||
|
"from_land_type": "black",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"required_level": 58,
|
||||||
|
"coin_cost": 4000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"required_level": 59,
|
||||||
|
"coin_cost": 4200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"required_level": 60,
|
||||||
|
"coin_cost": 4400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"required_level": 61,
|
||||||
|
"coin_cost": 4600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"required_level": 62,
|
||||||
|
"coin_cost": 4800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"required_level": 63,
|
||||||
|
"coin_cost": 5000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"required_level": 64,
|
||||||
|
"coin_cost": 5200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"required_level": 65,
|
||||||
|
"coin_cost": 5400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"required_level": 66,
|
||||||
|
"coin_cost": 5600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"required_level": 67,
|
||||||
|
"coin_cost": 5800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"required_level": 68,
|
||||||
|
"coin_cost": 6000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"required_level": 69,
|
||||||
|
"coin_cost": 6200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 13,
|
||||||
|
"required_level": 70,
|
||||||
|
"coin_cost": 6600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 14,
|
||||||
|
"required_level": 71,
|
||||||
|
"coin_cost": 7000000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 15,
|
||||||
|
"required_level": 72,
|
||||||
|
"coin_cost": 7400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 16,
|
||||||
|
"required_level": 73,
|
||||||
|
"coin_cost": 7800000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 17,
|
||||||
|
"required_level": 74,
|
||||||
|
"coin_cost": 8200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 18,
|
||||||
|
"required_level": 75,
|
||||||
|
"coin_cost": 8600000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 19,
|
||||||
|
"required_level": 77,
|
||||||
|
"coin_cost": 9500000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 20,
|
||||||
|
"required_level": 79,
|
||||||
|
"coin_cost": 10400000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 21,
|
||||||
|
"required_level": 81,
|
||||||
|
"coin_cost": 11300000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 22,
|
||||||
|
"required_level": 83,
|
||||||
|
"coin_cost": 12200000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"required_level": 85,
|
||||||
|
"coin_cost": 13100000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 24,
|
||||||
|
"required_level": 87,
|
||||||
|
"coin_cost": 14000000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -33,6 +33,20 @@ def receipt_text(receipt: Receipt) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_ids_text(snapshot: Snapshot) -> str:
|
||||||
|
"""无图片能力时按实际快照返回地块编号。"""
|
||||||
|
owned = sorted(plot.slot for plot in snapshot.plots)
|
||||||
|
remaining = sorted(set(range(1, 31)) - set(owned))
|
||||||
|
return '\n'.join((
|
||||||
|
'地块编号',
|
||||||
|
f'已开垦({len(owned)}块):' + ('、'.join(map(str, owned)) or '无'),
|
||||||
|
f'待开垦({len(remaining)}块):'
|
||||||
|
+ ('、'.join(map(str, remaining)) or '无'),
|
||||||
|
'前两排交错编号,之后逐排推进。',
|
||||||
|
'发送「农场详述」查看各地块的作物信息。',
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def snapshot_text(snapshot: Snapshot) -> str:
|
def snapshot_text(snapshot: Snapshot) -> str:
|
||||||
account = snapshot.account
|
account = snapshot.account
|
||||||
lines = [
|
lines = [
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class SQLiteRepository:
|
|||||||
)
|
)
|
||||||
self._db.executemany(
|
self._db.executemany(
|
||||||
"INSERT INTO plots(identity, slot) VALUES (?, ?)",
|
"INSERT INTO plots(identity, slot) VALUES (?, ?)",
|
||||||
[(self._identity, slot) for slot in range(1, 4)],
|
[(self._identity, slot) for slot in range(1, 7)],
|
||||||
)
|
)
|
||||||
self.asset(Asset.POINT, 500)
|
self.asset(Asset.POINT, 500)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -8282,46 +8282,46 @@
|
|||||||
"x": 1352,
|
"x": 1352,
|
||||||
"y": 552
|
"y": 552
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"x": 1462,
|
|
||||||
"y": 611
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 1572,
|
|
||||||
"y": 670
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 1682,
|
|
||||||
"y": 729
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 1792,
|
|
||||||
"y": 788
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 1902,
|
|
||||||
"y": 847
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"x": 1237,
|
"x": 1237,
|
||||||
"y": 606
|
"y": 606
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"x": 1462,
|
||||||
|
"y": 611
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"x": 1350,
|
"x": 1350,
|
||||||
"y": 665
|
"y": 665
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"x": 1572,
|
||||||
|
"y": 670
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"x": 1457,
|
"x": 1457,
|
||||||
"y": 724
|
"y": 724
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"x": 1682,
|
||||||
|
"y": 729
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"x": 1567,
|
"x": 1567,
|
||||||
"y": 783
|
"y": 783
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"x": 1792,
|
||||||
|
"y": 788
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"x": 1677,
|
"x": 1677,
|
||||||
"y": 842
|
"y": 842
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"x": 1902,
|
||||||
|
"y": 847
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"x": 1787,
|
"x": 1787,
|
||||||
"y": 901
|
"y": 901
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ class RenderOptions:
|
|||||||
quality: int = 75 # 有损编码质量,不代表分辨率。
|
quality: int = 75 # 有损编码质量,不代表分辨率。
|
||||||
method: int = 2 # WebP 编码力度,0最快,6最慢。
|
method: int = 2 # WebP 编码力度,0最快,6最慢。
|
||||||
theme: str = "default" # 显式主题,不读取隐含季节或全局配置。
|
theme: str = "default" # 显式主题,不读取隐含季节或全局配置。
|
||||||
|
show_plot_ids: bool = False # 编号图显示全部地块及开垦状态。
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
if type(self.show_plot_ids) is not bool:
|
||||||
|
raise ValueError("Invalid plot ID option")
|
||||||
for value, low, high in (
|
for value, low, high in (
|
||||||
(self.width, 256, 3072), (self.quality, 1, 95),
|
(self.width, 256, 3072), (self.quality, 1, 95),
|
||||||
(self.method, 0, 6),
|
(self.method, 0, 6),
|
||||||
@@ -48,7 +51,7 @@ class RenderOptions:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PlotView:
|
class PlotView:
|
||||||
slot: int # 旧布局的1到30地块编号。
|
slot: int # 1到30地块编号,对应统一位置表的slot-1项。
|
||||||
state: str # empty/growing/ripe/withered。
|
state: str # empty/growing/ripe/withered。
|
||||||
sprite: str = "" # 包内受控素材名,不来自用户路径。
|
sprite: str = "" # 包内受控素材名,不来自用户路径。
|
||||||
stage: int = 0 # 本季展示阶段。
|
stage: int = 0 # 本季展示阶段。
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ class FarmRenderer:
|
|||||||
self._pests(canvas, scene, options)
|
self._pests(canvas, scene, options)
|
||||||
self._decorations(canvas, options, "foreground")
|
self._decorations(canvas, options, "foreground")
|
||||||
self._panel(canvas, scene, options, avatar)
|
self._panel(canvas, scene, options, avatar)
|
||||||
|
if options.show_plot_ids:
|
||||||
|
self._plot_ids(canvas, scene, options)
|
||||||
composed = perf_counter()
|
composed = perf_counter()
|
||||||
content = canvas.encode(codec, options.quality, options.method)
|
content = canvas.encode(codec, options.quality, options.method)
|
||||||
encoded = perf_counter()
|
encoded = perf_counter()
|
||||||
@@ -92,6 +94,43 @@ class FarmRenderer:
|
|||||||
def _scale(value, options):
|
def _scale(value, options):
|
||||||
return round(value * options.width / 3072)
|
return round(value * options.width / 3072)
|
||||||
|
|
||||||
|
def _plot_ids(self, canvas, scene, options):
|
||||||
|
s = lambda n: self._scale(n, options)
|
||||||
|
owned = {plot.slot for plot in scene.plots}
|
||||||
|
soil_w, soil_h = self.assets.metadata['soil_size']
|
||||||
|
size = max(1, s(40))
|
||||||
|
for slot, pos in enumerate(self.assets.metadata['positions'], 1):
|
||||||
|
x = s(pos['x'] + soil_w/2)
|
||||||
|
y = s(pos['y'] + soil_h/2)
|
||||||
|
color = '#fff2ca' if slot in owned else '#d5e8bc'
|
||||||
|
canvas.rectangle(
|
||||||
|
(x-s(38), y-s(30), x+s(38), y+s(30)),
|
||||||
|
'#493b22', radius=s(30),
|
||||||
|
)
|
||||||
|
canvas.rectangle(
|
||||||
|
(x-s(36), y-s(28), x+s(36), y+s(28)),
|
||||||
|
color, radius=s(28),
|
||||||
|
)
|
||||||
|
label = str(slot)
|
||||||
|
width = canvas.measure_text(label, self.font_path, size)
|
||||||
|
box = font(self.font_path, size).getbbox(label)
|
||||||
|
canvas.text(
|
||||||
|
(round(x-width/2), round(y-(box[1]+box[3])/2)),
|
||||||
|
label, self.font_path, size, '#392917',
|
||||||
|
)
|
||||||
|
canvas.rectangle(
|
||||||
|
(s(76), s(1676), s(2996), s(1836)),
|
||||||
|
'#fff2ca', radius=s(24),
|
||||||
|
)
|
||||||
|
for y, text, text_size in (
|
||||||
|
(1694, '地块编号 · 浅色:已开垦 | 绿色:待开垦', 46),
|
||||||
|
(1760, '前两排交错编号,之后逐排推进。作物信息:农场详述', 40),
|
||||||
|
):
|
||||||
|
canvas.text(
|
||||||
|
(s(116), s(y)), text, self.font_path,
|
||||||
|
max(1, s(text_size)), '#392917',
|
||||||
|
)
|
||||||
|
|
||||||
def _base(self, scene, options):
|
def _base(self, scene, options):
|
||||||
slots = frozenset(p.slot for p in scene.plots)
|
slots = frozenset(p.slot for p in scene.plots)
|
||||||
key = ("base", options.theme, options.width, slots)
|
key = ("base", options.theme, options.width, slots)
|
||||||
|
|||||||
+33
-1
@@ -31,7 +31,7 @@ def snapshot(plots=None, **account):
|
|||||||
values = dict(name="演示农场", point=500, vip_point=0, experience=0)
|
values = dict(name="演示农场", point=500, vip_point=0, experience=0)
|
||||||
values.update(account)
|
values.update(account)
|
||||||
return Snapshot(Account(**values), (), tuple(
|
return Snapshot(Account(**values), (), tuple(
|
||||||
plots if plots is not None else [Plot(i) for i in range(1, 4)]
|
plots if plots is not None else [Plot(i) for i in range(1, 7)]
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +42,38 @@ class RenderTests(unittest.TestCase):
|
|||||||
self.who = Identity("test", "local", "qq", "one")
|
self.who = Identity("test", "local", "qq", "one")
|
||||||
self.plant = next(iter(Catalogue.bundled()))
|
self.plant = next(iter(Catalogue.bundled()))
|
||||||
|
|
||||||
|
def test_plot_ids_follow_positions_and_mark_owned_slots(self):
|
||||||
|
options = RenderOptions(width=1536, show_plot_ids=True)
|
||||||
|
scene = self.renderer.scene(self.who, snapshot(), 0)
|
||||||
|
canvas = Canvas.new((1536, 936), 'white')
|
||||||
|
with patch.object(canvas, 'text', wraps=canvas.text) as text:
|
||||||
|
self.renderer._plot_ids(canvas, scene, options)
|
||||||
|
self.assertEqual(
|
||||||
|
[call.args[1] for call in text.call_args_list[:30]],
|
||||||
|
[str(i) for i in range(1, 31)],
|
||||||
|
)
|
||||||
|
for slot, color in ((1, '#fff2ca'), (6, '#fff2ca'),
|
||||||
|
(7, '#d5e8bc'), (30, '#d5e8bc')):
|
||||||
|
pos = self.renderer.assets.metadata['positions'][slot-1]
|
||||||
|
x, y = round((pos['x']+117)/2), round((pos['y']+78)/2)
|
||||||
|
from PIL import ImageColor
|
||||||
|
self.assertEqual(canvas.image.getpixel((x-15, y))[:3],
|
||||||
|
ImageColor.getrgb(color))
|
||||||
|
|
||||||
|
def test_plot_id_image_has_separate_cache_from_normal_farm(self):
|
||||||
|
plain = self.renderer.render(self.who, snapshot(), 0)
|
||||||
|
options = RenderOptions(show_plot_ids=True)
|
||||||
|
numbered = self.renderer.render(self.who, snapshot(), 0, options)
|
||||||
|
self.assertFalse(numbered.cache_hit)
|
||||||
|
self.assertNotEqual(plain.content, numbered.content)
|
||||||
|
self.assertTrue(self.renderer.render(
|
||||||
|
self.who, snapshot(), 0, options,
|
||||||
|
).cache_hit)
|
||||||
|
self.assertEqual(
|
||||||
|
self.renderer.render(self.who, snapshot(), 0).content,
|
||||||
|
plain.content,
|
||||||
|
)
|
||||||
|
|
||||||
def test_canvas_alpha_crop_text_and_formats(self):
|
def test_canvas_alpha_crop_text_and_formats(self):
|
||||||
canvas = Canvas.new((100, 80), "white")
|
canvas = Canvas.new((100, 80), "white")
|
||||||
overlay = Image.new("RGBA", (20, 20), (255, 0, 0, 128))
|
overlay = Image.new("RGBA", (20, 20), (255, 0, 0, 128))
|
||||||
|
|||||||
@@ -61,13 +61,16 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(snapshot.account.point, 500)
|
self.assertEqual(snapshot.account.point, 500)
|
||||||
self.assertEqual(snapshot.account.vip_point, 0)
|
self.assertEqual(snapshot.account.vip_point, 0)
|
||||||
self.assertEqual(snapshot.account.experience, 0)
|
self.assertEqual(snapshot.account.experience, 0)
|
||||||
self.assertEqual(len(snapshot.plots), 3)
|
self.assertEqual([p.slot for p in snapshot.plots], list(range(1, 7)))
|
||||||
self.assertTrue(all(p.plant is None for p in snapshot.plots))
|
self.assertTrue(all(p.plant is None for p in snapshot.plots))
|
||||||
repeated = await self.service.register(
|
repeated = await self.service.register(
|
||||||
self.identity, "另一个名称", "register-again",
|
self.identity, "另一个名称", "register-again",
|
||||||
)
|
)
|
||||||
self.assertFalse(repeated.data["created"])
|
self.assertFalse(repeated.data["created"])
|
||||||
self.assertEqual(repeated.data["account"]["point"], 500)
|
self.assertEqual(repeated.data["account"]["point"], 500)
|
||||||
|
self.assertEqual(
|
||||||
|
(await self.service.snapshot(self.identity)).plots, snapshot.plots,
|
||||||
|
)
|
||||||
self.assertEqual(len(await self.store.ledger(self.identity)), 1)
|
self.assertEqual(len(await self.store.ledger(self.identity)), 1)
|
||||||
|
|
||||||
async def test_scopes_are_independent(self):
|
async def test_scopes_are_independent(self):
|
||||||
@@ -158,13 +161,13 @@ class TransactionTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(result.data["account"]["vip_point"], 6)
|
self.assertEqual(result.data["account"]["vip_point"], 6)
|
||||||
|
|
||||||
async def test_sow_all_and_explicit_overflow(self):
|
async def test_sow_all_and_explicit_overflow(self):
|
||||||
await self.service.buy(self.identity, "萝卜", 5, "buy")
|
await self.service.buy(self.identity, "萝卜", 8, "buy")
|
||||||
before = await self.service.snapshot(self.identity)
|
before = await self.service.snapshot(self.identity)
|
||||||
with self.assertRaisesRegex(FarmError, "insufficient_empty_plots"):
|
with self.assertRaisesRegex(FarmError, "insufficient_empty_plots"):
|
||||||
await self.service.sow(self.identity, "萝卜", 4, "too-many")
|
await self.service.sow(self.identity, "萝卜", 7, "too-many")
|
||||||
self.assertEqual(await self.service.snapshot(self.identity), before)
|
self.assertEqual(await self.service.snapshot(self.identity), before)
|
||||||
result = await self.service.sow(self.identity, "萝卜", None, "all")
|
result = await self.service.sow(self.identity, "萝卜", None, "all")
|
||||||
self.assertEqual(result.data["quantity"], 3)
|
self.assertEqual(result.data["quantity"], 6)
|
||||||
self.assertEqual(result.data["seeds"], 2)
|
self.assertEqual(result.data["seeds"], 2)
|
||||||
|
|
||||||
async def test_maturity_boundary_and_multi_crop_lifecycle(self):
|
async def test_maturity_boundary_and_multi_crop_lifecycle(self):
|
||||||
|
|||||||
@@ -45,6 +45,44 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
**{**self.context, "message_id":message, **changes},
|
**{**self.context, "message_id":message, **changes},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def test_plot_ids_command_registration_hint_and_read_only(self):
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
self.assertEqual(self.module.COMMANDS['地块编号'], 'plot_ids')
|
||||||
|
self.assertIn('地块编号', await self.call('help'))
|
||||||
|
first = await self.call('register')
|
||||||
|
self.assertIn('6块土地', first)
|
||||||
|
self.assertIn('地块编号', first)
|
||||||
|
repeated = await self.call('register', message='again')
|
||||||
|
self.assertNotIn('已获得6块土地', repeated)
|
||||||
|
before = await self.runtime.service.snapshot(self.runtime_identity())
|
||||||
|
with patch.object(self.runtime.service, 'snapshot',
|
||||||
|
new=AsyncMock(return_value=before)) as query:
|
||||||
|
result = await self.call('plot_ids', message=None)
|
||||||
|
query.assert_awaited_once()
|
||||||
|
with Image.open(BytesIO(result)) as image:
|
||||||
|
self.assertEqual(image.width, 1024)
|
||||||
|
self.assertEqual(image.format, 'JPEG')
|
||||||
|
self.assertEqual(
|
||||||
|
await self.runtime.service.snapshot(self.runtime_identity()),
|
||||||
|
before,
|
||||||
|
)
|
||||||
|
self.assertIn('参数有误', await self.call('plot_ids', '1'))
|
||||||
|
self.assertIn('开通农场', await self.call('plot_ids', user_id='other'))
|
||||||
|
|
||||||
|
async def test_plot_ids_text_fallback_uses_actual_owned_slots(self):
|
||||||
|
await self.call('register')
|
||||||
|
rendering = self.runtime.rendering
|
||||||
|
self.runtime.rendering = None
|
||||||
|
try:
|
||||||
|
text = await self.call('plot_ids', message=None)
|
||||||
|
finally:
|
||||||
|
self.runtime.rendering = rendering
|
||||||
|
self.assertIn('已开垦(6块):1、2、3、4、5、6', text)
|
||||||
|
self.assertIn('待开垦(24块):7、8、9', text)
|
||||||
|
self.assertIn('农场详述', text)
|
||||||
|
|
||||||
async def test_original_commands_transactions_and_png(self):
|
async def test_original_commands_transactions_and_png(self):
|
||||||
self.assertNotIn("测试农场", self.module.COMMANDS)
|
self.assertNotIn("测试农场", self.module.COMMANDS)
|
||||||
self.assertIn("开通农场", self.module.COMMANDS)
|
self.assertIn("开通农场", self.module.COMMANDS)
|
||||||
|
|||||||
Reference in New Issue
Block a user