mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-11-10 15:00:10 +08:00
add # 新增种子商店界面
fixed # 修复数据库目录错误的BUG
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
# 文字指令种子商店
|
||||||
|
|
||||||
|
采用用户选定的方案二:木质价目板、单列六种。发送 `种子商店` 或
|
||||||
|
`种子商店 2` 返回 1024×1536 PNG;标题、余额、商品信息、购买示例和
|
||||||
|
页码都由程序绘制。原始作物 icon 复用包内资源;缺图显示“暂无图”。
|
||||||
|
|
||||||
|
显示农场等级、农场币和点券,商品显示稳定本地编号、名称、实际单价、
|
||||||
|
币种、购买等级、首熟时长、可收获季数和每季基础产量。
|
||||||
|
首熟时长和产量是目录基础值,虫害等实际种植结算仍由核心处理。
|
||||||
|
等级不足时突出“需 Lv.…”。未开通用户可浏览,购买仍需先开通农场。
|
||||||
|
|
||||||
|
只展示 buyable 作物,按本地编号排序,每页六种。最后一页不补假商品,
|
||||||
|
越界页码返回中文提示,页数按实际目录计算。字体不可用时返回同样分页的
|
||||||
|
文字列表。图片通过既有有限任务队列绘制,不在事件循环里执行 Pillow。
|
||||||
|
相同待处理请求合并,关闭时等待任务排空。
|
||||||
|
|
||||||
|
购买入口保持核心事务和消息去重不变:
|
||||||
|
|
||||||
|
- `购买种子 #1 5`
|
||||||
|
- `购买种子 胡萝卜 5`
|
||||||
|
- 名称有空格时使用引号;重名作物须用编号。
|
||||||
|
|
||||||
|
木牌空白底图为内置图像生成工具生成,非 QQ 农场原始资源。源文件
|
||||||
|
`src/farm_render/source_assets/webgl/shop-board.png`,由素材导出脚本加入
|
||||||
|
assets.zip,并在 metadata.json 保存 SHA-256。底图不含商品、价格和文字。
|
||||||
|
生成提示要点:1024×1536,QQ 农场风格暖色手绘木牌、橙色标题、米色纸面、
|
||||||
|
单列六个空白商品条、底部文字指令区;禁止生成文字、数字和商品图。
|
||||||
|
|
||||||
|
执行 `scripts/preview_shop.py --output <目录>` 可复现首页、第二页、末页与
|
||||||
|
未开通预览。预览余额为演示值,商品使用实际目录。此改动不部署、不重启。
|
||||||
@@ -16,6 +16,7 @@ from zhenxun.utils.platform import PlatformUtils
|
|||||||
|
|
||||||
from .runtime import COMMANDS, Runtime
|
from .runtime import COMMANDS, Runtime
|
||||||
from .data_paths import data_directories
|
from .data_paths import data_directories
|
||||||
|
from .config_migration import migrate_config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
driver = get_driver()
|
driver = get_driver()
|
||||||
@@ -30,7 +31,14 @@ runtime = Runtime(
|
|||||||
font_dir=FONT_PATH,
|
font_dir=FONT_PATH,
|
||||||
legacy_data_dir=legacy_data_dir,
|
legacy_data_dir=legacy_data_dir,
|
||||||
)
|
)
|
||||||
driver.on_startup(runtime.start)
|
async def start_runtime():
|
||||||
|
from zhenxun.configs.config import Config
|
||||||
|
|
||||||
|
migrate_config(Config, __package__.split(".")[-1])
|
||||||
|
await runtime.start()
|
||||||
|
|
||||||
|
|
||||||
|
driver.on_startup(start_runtime)
|
||||||
driver.on_shutdown(runtime.close)
|
driver.on_shutdown(runtime.close)
|
||||||
_avatars = {}
|
_avatars = {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""仅清理农场已废弃配置,保存备份并同步网页配置数据。"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
OBSOLETE_KEYS = (
|
||||||
|
"绘制农场清晰度", "动态背景", "签到图片样式", "兑换倍数",
|
||||||
|
"点券兑换倍数", "手续费", "服务地址",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_config(manager, module):
|
||||||
|
group = manager.get(module)
|
||||||
|
simple = manager._simple_data.get(module, {})
|
||||||
|
removed = [key for key in OBSOLETE_KEYS
|
||||||
|
if key in group.configs or key in simple]
|
||||||
|
if not removed:
|
||||||
|
return
|
||||||
|
for source in (manager.file, manager._simple_file):
|
||||||
|
source = Path(source)
|
||||||
|
backup = source.with_name(source.name + ".farm-v1.bak")
|
||||||
|
if source.exists() and not backup.exists():
|
||||||
|
shutil.copy2(source, backup)
|
||||||
|
for key in removed:
|
||||||
|
group.configs.pop(key, None)
|
||||||
|
simple.pop(key, None)
|
||||||
|
manager.save(save_simple_data=True)
|
||||||
|
logging.getLogger(__name__).info(
|
||||||
|
"farm config migration module=%s removed_keys=%s", module, removed,
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ from ._vendor.farm_core.storage import SCHEMA_VERSION
|
|||||||
from ._vendor.farm_core.presentation import receipt_text, snapshot_text
|
from ._vendor.farm_core.presentation import 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 .data_paths import prepare_directory
|
from .data_paths import prepare_directory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -167,17 +168,29 @@ class Runtime:
|
|||||||
return HELP
|
return HELP
|
||||||
if action == "shop":
|
if action == "shop":
|
||||||
page = int(args[0]) if args else 1
|
page = int(args[0]) if args else 1
|
||||||
if not 1 <= page <= 100 or len(args) > 1:
|
if len(args) > 1:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
plants = sorted(
|
try:
|
||||||
(p for p in self.service.catalogue if p.buyable),
|
chunk, pages = shop_page(self.service.catalogue, page)
|
||||||
key=lambda p: p.plant_id,
|
except ValueError:
|
||||||
)
|
return '页码无效,请发送:种子商店 1'
|
||||||
chunk = plants[(page-1)*12:page*12]
|
if self.rendering:
|
||||||
return "种子商店 " + str(page) + "页\n" + "\n".join(
|
account = None
|
||||||
f"{p.name}(#{p.plant_id}):"
|
try:
|
||||||
f"{p.cost}{'点券' if p.is_vip else '农场币'}"
|
account = (await self.service.snapshot(who)).account
|
||||||
f" / 等级{p.level}" for p in chunk
|
except FarmError as error:
|
||||||
|
if error.code != 'not_registered':
|
||||||
|
raise
|
||||||
|
return await self.rendering.seed_shop(
|
||||||
|
chunk, account, page, pages,
|
||||||
|
)
|
||||||
|
return f'种子商店 第{page}/{pages}页\n' + '\n'.join(
|
||||||
|
f'{p.name}(#{p.plant_id}):'
|
||||||
|
f'{p.cost}{"点券" if p.is_vip else "农场币"}'
|
||||||
|
f' / 等级{p.level}' for p in chunk
|
||||||
|
) + (
|
||||||
|
'\n购买种子 #编号 数量 或 购买种子 种子名 数量'
|
||||||
|
+ (f'\n下一页:种子商店 {page+1}' if page < pages else '')
|
||||||
)
|
)
|
||||||
if action == "inventory":
|
if action == "inventory":
|
||||||
if args:
|
if args:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def build(root, output):
|
|||||||
integration = root / "integrations/zhenxun"
|
integration = root / "integrations/zhenxun"
|
||||||
for name in (
|
for name in (
|
||||||
"__init__.py", "runtime.py", "commands.py", "data_paths.py",
|
"__init__.py", "runtime.py", "commands.py", "data_paths.py",
|
||||||
"requirements.txt",
|
"requirements.txt", "config_migration.py",
|
||||||
):
|
):
|
||||||
files[name] = (integration / name).read_bytes()
|
files[name] = (integration / name).read_bytes()
|
||||||
files["_vendor/__init__.py"] = b'"""Private shared farm modules."""\n'
|
files["_vendor/__init__.py"] = b'"""Private shared farm modules."""\n'
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ def export(root):
|
|||||||
metadata["panel"] = manifest["panel"]
|
metadata["panel"] = manifest["panel"]
|
||||||
entries = {path.relative_to(source).as_posix(): path for path in paths}
|
entries = {path.relative_to(source).as_posix(): path for path in paths}
|
||||||
entries.update({name: webgl_source / name for name in manifest["images"]})
|
entries.update({name: webgl_source / name for name in manifest["images"]})
|
||||||
|
entries['webgl/shop-board.png'] = webgl_source / 'webgl/shop-board.png'
|
||||||
with ZipFile(output, "w", compression=ZIP_STORED) as bundle:
|
with ZipFile(output, "w", compression=ZIP_STORED) as bundle:
|
||||||
for name, path in sorted(entries.items()):
|
for name, path in sorted(entries.items()):
|
||||||
content = path.read_bytes()
|
content = path.read_bytes()
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""用实际目录和离线示例账户生成种子商店预览。"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / 'src'))
|
||||||
|
|
||||||
|
from ci_font import choose_font
|
||||||
|
from farm_core import Catalogue
|
||||||
|
from farm_core.domain import Account
|
||||||
|
from farm_render import FarmRenderer
|
||||||
|
from farm_render.shop import render_shop, shop_page
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--output', type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.output.mkdir(parents=True, exist_ok=True)
|
||||||
|
renderer = FarmRenderer(choose_font())
|
||||||
|
catalogue = Catalogue.bundled()
|
||||||
|
account = Account('演示农场', 12580, 36, 3420)
|
||||||
|
_, pages = shop_page(catalogue, 1)
|
||||||
|
for page in dict.fromkeys((1, min(2, pages), pages)):
|
||||||
|
plants, _ = shop_page(catalogue, page)
|
||||||
|
(args.output / f'shop-page-{page}.png').write_bytes(render_shop(
|
||||||
|
renderer, plants, account, page, pages,
|
||||||
|
))
|
||||||
|
plants, _ = shop_page(catalogue, 1)
|
||||||
|
(args.output / 'shop-unregistered.png').write_bytes(render_shop(
|
||||||
|
renderer, plants, None, 1, pages,
|
||||||
|
))
|
||||||
|
print(f'Shop previews: {args.output}')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Binary file not shown.
@@ -6244,6 +6244,13 @@
|
|||||||
30
|
30
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"webgl/shop-board.png": {
|
||||||
|
"sha256": "88730b82d88cb33fae45c7180a88954fcc0241b2b4f5de095fc6b22b30296645",
|
||||||
|
"size": [
|
||||||
|
1024,
|
||||||
|
1536
|
||||||
|
]
|
||||||
|
},
|
||||||
"webgl/wither.png": {
|
"webgl/wither.png": {
|
||||||
"sha256": "1c87f05e0b7bd8287926351e07746bc9e36371a860afaef0e4b00c51536dc472",
|
"sha256": "1c87f05e0b7bd8287926351e07746bc9e36371a860afaef0e4b00c51536dc472",
|
||||||
"size": [
|
"size": [
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
import time
|
import time
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .shop import render_shop
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -60,13 +63,39 @@ class RenderService:
|
|||||||
return await asyncio.shield(task)
|
return await asyncio.shield(task)
|
||||||
|
|
||||||
async def _run(self, *args):
|
async def _run(self, *args):
|
||||||
|
return await self._run_function(self.renderer.render, *args)
|
||||||
|
|
||||||
|
async def seed_shop(self, plants, account, page, pages):
|
||||||
|
if self._closed:
|
||||||
|
raise RuntimeError('Render service closed')
|
||||||
|
key = 'shop:' + hashlib.sha256(
|
||||||
|
repr((plants, account, page, pages)).encode(),
|
||||||
|
).hexdigest()
|
||||||
|
task = self._pending.get(key)
|
||||||
|
if task is None:
|
||||||
|
if len(self._pending) >= self._max_pending:
|
||||||
|
raise RuntimeError('Render queue full')
|
||||||
|
task = asyncio.create_task(self._run_function(
|
||||||
|
render_shop, self.renderer, plants, account, page, pages,
|
||||||
|
))
|
||||||
|
self._pending[key] = task
|
||||||
|
|
||||||
|
def finished(done):
|
||||||
|
self._pending.pop(key, None)
|
||||||
|
if not done.cancelled():
|
||||||
|
done.exception()
|
||||||
|
|
||||||
|
task.add_done_callback(finished)
|
||||||
|
return await asyncio.shield(task)
|
||||||
|
|
||||||
|
async def _run_function(self, function, *args):
|
||||||
started = perf_counter()
|
started = perf_counter()
|
||||||
async with self._semaphore:
|
async with self._semaphore:
|
||||||
logger.info(
|
logger.info(
|
||||||
"farm render queue_ms=%.2f", (perf_counter()-started)*1000,
|
"farm render queue_ms=%.2f", (perf_counter()-started)*1000,
|
||||||
)
|
)
|
||||||
worker = asyncio.create_task(asyncio.to_thread(
|
worker = asyncio.create_task(asyncio.to_thread(
|
||||||
self.renderer.render, *args,
|
function, *args,
|
||||||
))
|
))
|
||||||
try:
|
try:
|
||||||
return await asyncio.shield(worker)
|
return await asyncio.shield(worker)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""文字指令使用的木质种子价目板;全部价格来自离线目录。"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .canvas import Canvas, font
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
PAGE_SIZE = 6
|
||||||
|
|
||||||
|
|
||||||
|
def shop_page(catalogue, page):
|
||||||
|
plants = sorted((p for p in catalogue if p.buyable),
|
||||||
|
key=lambda p: p.plant_id)
|
||||||
|
pages = max(1, (len(plants) + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
if type(page) is not int or not 1 <= page <= pages:
|
||||||
|
raise ValueError('Invalid shop page')
|
||||||
|
start = (page - 1) * PAGE_SIZE
|
||||||
|
return tuple(plants[start:start + PAGE_SIZE]), pages
|
||||||
|
|
||||||
|
|
||||||
|
def duration(seconds):
|
||||||
|
hours, remainder = divmod(seconds, 3600)
|
||||||
|
minutes, seconds = divmod(remainder, 60)
|
||||||
|
return ''.join(f'{value}{unit}' for value, unit in (
|
||||||
|
(hours, '小时'), (minutes, '分'), (seconds, '秒'),
|
||||||
|
) if value) or '0秒'
|
||||||
|
|
||||||
|
|
||||||
|
def render_shop(renderer, plants, account, page, pages):
|
||||||
|
canvas = Canvas(renderer.assets.image('webgl/shop-board.png', (1024, 1536)))
|
||||||
|
|
||||||
|
def text(value, xy, size=28, color='#5b3015', width=850):
|
||||||
|
value = canvas.fit_text(value, renderer.font_path, size, width)
|
||||||
|
box = font(renderer.font_path, size).getbbox(value)
|
||||||
|
canvas.text((xy[0], xy[1]-box[1]), value,
|
||||||
|
renderer.font_path, size, color)
|
||||||
|
|
||||||
|
def centered(value, y, size, color='#5b3015'):
|
||||||
|
x = round((1024 - canvas.measure_text(
|
||||||
|
value, renderer.font_path, size,
|
||||||
|
)) / 2)
|
||||||
|
text(value, (x, y), size, color)
|
||||||
|
|
||||||
|
centered('种 子 商 店', 58, 54, '#fff5d3')
|
||||||
|
centered('种子价目板 · 发送文字指令购买', 178, 25, '#7a6439')
|
||||||
|
balances = ('未开通农场 · 可浏览种子,购买前请发送:开通农场'
|
||||||
|
if account is None else
|
||||||
|
f'Lv.{account.level} 农场币 {account.point:,} '
|
||||||
|
f'点券 {account.vip_point:,}')
|
||||||
|
text(balances, (142, 248), 29, width=745)
|
||||||
|
for index, plant in enumerate(plants):
|
||||||
|
y = 326 + index * 156
|
||||||
|
icon = f'plant/{plant.name}/icon.png'
|
||||||
|
if icon in renderer.assets.metadata['images']:
|
||||||
|
image = renderer.assets.image(icon)
|
||||||
|
image.thumbnail((104, 110), Image.Resampling.LANCZOS)
|
||||||
|
canvas.paste(image, (175-image.width//2, y+67-image.height//2))
|
||||||
|
else:
|
||||||
|
text('暂无图', (133, y+55), 23, '#8b8057', width=92)
|
||||||
|
logger.info('farm shop packaged icon unavailable id=%s',
|
||||||
|
plant.plant_id)
|
||||||
|
text(plant.name, (268, y+13), 34, width=490)
|
||||||
|
text(f'#{plant.plant_id}', (774, y+17), 26, '#69783e', width=132)
|
||||||
|
currency = '点券' if plant.is_vip else '农场币'
|
||||||
|
text(f'{plant.cost:,} {currency} / 粒', (268, y+62), 32,
|
||||||
|
'#b66522', width=480)
|
||||||
|
locked = account is not None and account.level < plant.level
|
||||||
|
text(f'{"需" if locked else "等级"} Lv.{plant.level}',
|
||||||
|
(774, y+69), 23, '#ac542d' if locked else '#66733d', width=132)
|
||||||
|
text(f'首熟 {duration(plant.growth_seconds)} · {plant.crop}季'
|
||||||
|
f' · 每季{plant.harvest}个', (268, y+108), 23, width=635)
|
||||||
|
if not plants:
|
||||||
|
text('暂无出售中的种子', (340, 690), 34)
|
||||||
|
example = plants[0] if plants else None
|
||||||
|
if example:
|
||||||
|
text(f'按编号:购买种子 #{example.plant_id} 5', (142, 1291), 27)
|
||||||
|
name = example.name
|
||||||
|
if any(c.isspace() for c in name) or any(c in name for c in '\"\''):
|
||||||
|
name = shlex.quote(name)
|
||||||
|
text(f'按名称:购买种子 {name} 5', (142, 1341), 27, width=750)
|
||||||
|
else:
|
||||||
|
text('购买种子 #编号 数量 或 购买种子 种子名 数量',
|
||||||
|
(142, 1316), 25, width=750)
|
||||||
|
navigation = f'第 {page} / {pages} 页'
|
||||||
|
if page < pages:
|
||||||
|
navigation += f' 下一页:种子商店 {page+1}'
|
||||||
|
elif page > 1:
|
||||||
|
navigation += f' 上一页:种子商店 {page-1}'
|
||||||
|
text(navigation, (263, 1420), 23, width=510)
|
||||||
|
return canvas.encode('PNG')
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,47 @@
|
|||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"farm_config_migration",
|
||||||
|
ROOT / "integrations/zhenxun/config_migration.py",
|
||||||
|
)
|
||||||
|
migration = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(migration)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigMigrationTests(unittest.TestCase):
|
||||||
|
def test_cleanup_backups_and_repeat_are_scoped(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
group = SimpleNamespace(configs={
|
||||||
|
"绘制农场清晰度": "low", "CUSTOM": "keep",
|
||||||
|
})
|
||||||
|
saves = []
|
||||||
|
manager = SimpleNamespace(
|
||||||
|
file=root / "plugins2config.yaml",
|
||||||
|
_simple_file=root / "config.yaml",
|
||||||
|
_simple_data={
|
||||||
|
"farm": {"绘制农场清晰度": "low", "CUSTOM": "keep"},
|
||||||
|
"other": {"服务地址": "keep"},
|
||||||
|
},
|
||||||
|
get=lambda module: group,
|
||||||
|
save=lambda **kwargs: saves.append(kwargs),
|
||||||
|
)
|
||||||
|
for path in (manager.file, manager._simple_file):
|
||||||
|
path.write_text("original", encoding="utf-8")
|
||||||
|
migration.migrate_config(manager, "farm")
|
||||||
|
self.assertEqual(group.configs, {"CUSTOM": "keep"})
|
||||||
|
self.assertEqual(manager._simple_data["farm"], {"CUSTOM": "keep"})
|
||||||
|
self.assertEqual(manager._simple_data["other"], {"服务地址": "keep"})
|
||||||
|
self.assertEqual(saves, [{"save_simple_data": True}])
|
||||||
|
for path in (manager.file, manager._simple_file):
|
||||||
|
self.assertEqual(path.with_name(
|
||||||
|
path.name + ".farm-v1.bak",
|
||||||
|
).read_text(encoding="utf-8"), "original")
|
||||||
|
migration.migrate_config(manager, "farm")
|
||||||
|
self.assertEqual(len(saves), 1)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from dataclasses import replace
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||||
|
from ci_font import choose_font
|
||||||
|
from farm_core import Catalogue
|
||||||
|
from farm_core.domain import Account
|
||||||
|
from farm_render import FarmRenderer
|
||||||
|
from farm_render.shop import shop_page, render_shop
|
||||||
|
from farm_render.canvas import Canvas
|
||||||
|
|
||||||
|
|
||||||
|
class ShopRenderTests(unittest.TestCase):
|
||||||
|
def test_pages_filter_unavailable_and_keep_stable_ids(self):
|
||||||
|
plant = Catalogue.bundled().get('胡萝卜')
|
||||||
|
plants = [replace(plant, plant_id=i) for i in range(1, 9)]
|
||||||
|
plants += [replace(plant, plant_id=99, purchasable=False)]
|
||||||
|
rows, total = shop_page(plants, 2)
|
||||||
|
self.assertEqual(total, 2)
|
||||||
|
self.assertEqual([p.plant_id for p in rows], [7, 8])
|
||||||
|
for page in (0, 3, True):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
shop_page(plants, page)
|
||||||
|
self.assertEqual(shop_page([], 1), ((), 1))
|
||||||
|
|
||||||
|
def test_shop_uses_actual_fields_and_missing_icon_fallback(self):
|
||||||
|
renderer = FarmRenderer(choose_font())
|
||||||
|
plant = Catalogue.bundled().get('胡萝卜')
|
||||||
|
rows = (plant, replace(plant, plant_id=99999,
|
||||||
|
name='不存在的素材', is_vip=True, vip_buy=35))
|
||||||
|
with patch.object(Canvas, 'text', autospec=True) as text:
|
||||||
|
result = render_shop(
|
||||||
|
renderer, rows, Account('商店', 12580, 36, 0), 1, 1,
|
||||||
|
)
|
||||||
|
labels = [call.args[2] for call in text.call_args_list]
|
||||||
|
self.assertIn(f'{plant.cost:,} 农场币 / 粒', labels)
|
||||||
|
self.assertIn('35 点券 / 粒', labels)
|
||||||
|
self.assertIn('暂无图', labels)
|
||||||
|
self.assertIn(f'按编号:购买种子 #{plant.plant_id} 5', labels)
|
||||||
|
self.assertIn('按名称:购买种子 胡萝卜 5', labels)
|
||||||
|
with Image.open(BytesIO(result)) as image:
|
||||||
|
self.assertEqual(image.size, (1024, 1536))
|
||||||
|
self.assertEqual(image.format, 'PNG')
|
||||||
@@ -69,7 +69,12 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
replace(carrot, plant_id=9002, enabled=False, name="停用菜"),
|
replace(carrot, plant_id=9002, enabled=False, name="停用菜"),
|
||||||
replace(carrot, plant_id=9003, crop_type=11, name="特殊道具"),
|
replace(carrot, plant_id=9003, crop_type=11, name="特殊道具"),
|
||||||
])
|
])
|
||||||
shop = await self.call("shop")
|
rendering = self.runtime.rendering
|
||||||
|
self.runtime.rendering = None
|
||||||
|
try:
|
||||||
|
shop = await self.call("shop")
|
||||||
|
finally:
|
||||||
|
self.runtime.rendering = rendering
|
||||||
self.assertIn("胡萝卜(#1)", shop)
|
self.assertIn("胡萝卜(#1)", shop)
|
||||||
for name in ("禁售菜", "停用菜", "特殊道具"):
|
for name in ("禁售菜", "停用菜", "特殊道具"):
|
||||||
self.assertNotIn(name, shop)
|
self.assertNotIn(name, shop)
|
||||||
@@ -78,6 +83,28 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertIn("不出售", await self.call("buy", "禁售菜 1", "blocked"))
|
self.assertIn("不出售", await self.call("buy", "禁售菜 1", "blocked"))
|
||||||
self.assertIn("(#1)", await self.call("inventory"))
|
self.assertIn("(#1)", await self.call("inventory"))
|
||||||
|
|
||||||
|
async def test_shop_image_page_and_buy_by_name_or_id(self):
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
result = await self.call('shop')
|
||||||
|
with Image.open(BytesIO(result)) as image:
|
||||||
|
self.assertEqual(image.size, (1024, 1536))
|
||||||
|
await self.call('register')
|
||||||
|
self.assertIn('已完成', await self.call('buy', '白萝卜 1', 'name'))
|
||||||
|
plant = self.runtime.service.catalogue.get('白萝卜')
|
||||||
|
self.assertIn('已完成', await self.call(
|
||||||
|
'buy', f'#{plant.plant_id} 1', 'id',
|
||||||
|
))
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
self.runtime.rendering.seed_shop = AsyncMock(return_value=b'shop')
|
||||||
|
self.assertEqual(await self.call('shop', '2'), b'shop')
|
||||||
|
args = self.runtime.rendering.seed_shop.call_args.args
|
||||||
|
self.assertEqual(len(args[0]), 6)
|
||||||
|
self.assertEqual(args[1].name, '离线玩家')
|
||||||
|
self.assertEqual(args[2], 2)
|
||||||
|
for page in ('0', '999999'):
|
||||||
|
self.assertIn('页码无效', await self.call('shop', page))
|
||||||
|
|
||||||
async def test_farm_uses_calendar_theme_and_bee_is_not_a_user_option(self):
|
async def test_farm_uses_calendar_theme_and_bee_is_not_a_user_option(self):
|
||||||
rendering = self.runtime.rendering
|
rendering = self.runtime.rendering
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
@@ -251,8 +278,13 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
))
|
))
|
||||||
platform = ModuleType("zhenxun.utils.platform")
|
platform = ModuleType("zhenxun.utils.platform")
|
||||||
platform.PlatformUtils = SimpleNamespace()
|
platform.PlatformUtils = SimpleNamespace()
|
||||||
|
config = ModuleType("zhenxun.configs.config")
|
||||||
|
config.Config = SimpleNamespace(
|
||||||
|
get=lambda module: SimpleNamespace(configs={}),
|
||||||
|
_simple_data={},
|
||||||
|
)
|
||||||
modules = {m.__name__:m for m in (
|
modules = {m.__name__:m for m in (
|
||||||
nonebot, adapters, params, uninfo, paths, messages, platform,
|
nonebot, adapters, params, uninfo, paths, messages, platform, config,
|
||||||
)}
|
)}
|
||||||
with patch.dict(sys.modules, modules):
|
with patch.dict(sys.modules, modules):
|
||||||
imported = importlib.import_module(self.package + ".commands")
|
imported = importlib.import_module(self.package + ".commands")
|
||||||
|
|||||||
Reference in New Issue
Block a user