Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
}
8 changes: 8 additions & 0 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
- `/jrrp (获取今天的人品值)`
- `/jrrp r (今日幸运星[--rank])`
- `/jrrp rr (今日倒霉蛋[--rank-r])`
## `market`: 全球市场

玩家间物品交易市场:可以将自己背包中的物品上架出售,也可以购买其他玩家出售的物品

### 用法
- `/market`
- `/market sell <bag_index> [count] [price_diff]`
- `/market buy <name> [count]`
## `minigame`: 小游戏积分排名

查看 Moonlark 中游玩玩法的用户的排名
Expand Down
55 changes: 55 additions & 0 deletions migrations/versions/9f6eedd5686c_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""empty message

迁移 ID: 9f6eedd5686c
父迁移: f79433854a65
创建时间: 2025-08-14 13:39:38.284103

"""

from __future__ import annotations

from collections.abc import Sequence

from alembic import op
import sqlalchemy as sa


revision: str = "9f6eedd5686c"
down_revision: str | Sequence[str] | None = "f79433854a65"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade(name: str = "") -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"nonebot_plugin_market_marketitem",
sa.Column("item_id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("item_namespace", sa.String(length=64), nullable=False),
sa.Column("remain_count", sa.Integer(), nullable=False),
sa.Column("price", sa.Float(), nullable=False),
sa.Column("user_id", sa.String(length=128), nullable=False),
sa.Column("item_data", sa.LargeBinary(), nullable=False),
sa.PrimaryKeyConstraint("item_id", name=op.f("pk_nonebot_plugin_market_marketitem")),
info={"bind_key": "nonebot_plugin_market"},
)
op.create_table(
"nonebot_plugin_market_selllog",
sa.Column("item_namespace", sa.String(length=64), nullable=False),
sa.Column("sold_count", sa.Integer(), nullable=False),
sa.Column("price_sum", sa.Float(), nullable=False),
sa.PrimaryKeyConstraint("item_namespace", name=op.f("pk_nonebot_plugin_market_selllog")),
info={"bind_key": "nonebot_plugin_market"},
)
# ### end Alembic commands ###


def downgrade(name: str = "") -> None:
if name:
return
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("nonebot_plugin_market_selllog")
op.drop_table("nonebot_plugin_market_marketitem")
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ plugins = [
"nonebot_plugin_waiter",
"nonebot_plugin_bilichat",
"nonebot_plugin_picmcstat",
"nonebot_plugin_fakepic",

# Custom plugins
"nonebot_plugin_access",
Expand Down Expand Up @@ -82,6 +81,7 @@ plugins = [
"nonebot_plugin_hkrpg_calendar",
"nonebot_plugin_ba_calendar",
"nonebot_plugin_everyday_wife",
"nonebot_plugin_market",
"nonebot_plugin_online_timer",
"nonebot_plugin_status_report",
"nonebot_plugin_broadcast",
Expand Down
30 changes: 30 additions & 0 deletions src/lang/zh_hans/market.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
help:
description: 全球市场
details: |
玩家间物品交易市场:可以将自己背包中的物品上架出售,也可以购买其他玩家出售的物品。
所有交易收取 1% 手续费。
usage1: market — 查看市场商品总数
usage2: market list [page] — 浏览市场中出售的物品(每页 10 件)
usage3: market sell <bag_index> [count] [price_diff] — 上架物品
usage4: market buy <name> [count] — 购买物品
usage5: |
price_diff 说明(基于该物品的平均成交价浮动):
留空 = 按均价上架
+ = 高于均价 1%(每多一个 + 再加 1%,最多 +5%)
- = 低于均价 1%(每多一个 - 再减 1%,最多 -5%)
+++ = 高于均价 3%
-- = 低于均价 2%
main:
info: 当前市场上有 {} 件商品正在出售
sell:
index_error: 背包中不存在此物品
item_not_enough: 出售失败,选中的物品只有 {} 个,数量不足
wrong_count: 出售数量不能为负数!
not_soldable: 该物品不支持在市场上出售
done: 完成!已将 {} 个「{}」以单价 {} VimCoin 的价格上架至全球市场
buy:
finish: 完成!购买了 {} 个「{}」,总共花费了 {} VimCoin
list:
empty: 市场上暂时没有商品在出售
header: "📦 全球市场(第 {}/{} 页,共 {} 件)\n{}\n提示:使用 market list <页码> 翻页"
item_line: "{}. {} ×{} — {} VimCoin"
19 changes: 19 additions & 0 deletions src/plugins/nonebot_plugin_market/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from nonebot import require
from nonebot.plugin import PluginMetadata


__plugin_meta__ = PluginMetadata(
name="nonebot-plugin-market",
description="玩家间物品交易市场",
usage="market [list [page]] | market sell <bag_index> [count] [price_diff] | market buy <name> [count]",
config=None,
)

require("nonebot_plugin_orm")
require("nonebot_plugin_alconna")
require("nonebot_plugin_larklang")
require("nonebot_plugin_larkutils")
require("nonebot_plugin_bag")
require("nonebot_plugin_items")

from . import __main__
150 changes: 150 additions & 0 deletions src/plugins/nonebot_plugin_market/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import json
from nonebot_plugin_alconna import Alconna, on_alconna, Args, Subcommand
from nonebot_plugin_items.base.stack import ItemStack
from nonebot_plugin_items.utils.get import get_item
from nonebot_plugin_items.utils.string import get_location_by_id
from nonebot_plugin_larkuser.utils.user import get_user
from nonebot_plugin_larkutils import get_user_id
from nonebot_plugin_larklang import LangHelper
from nonebot_plugin_bag.utils.bag import get_bag_item, give_item
from nonebot_plugin_orm import get_session, AsyncSession
from .models import MarketItem, SellLog
from sqlalchemy import select


lang = LangHelper()
matcher = on_alconna(
Alconna(
"market",
Subcommand("sell", Args["bag_index", int], Args["count", int, 0], Args["price_diff", str, ""]),
Subcommand("buy", Args["name", str], Args["count", int, 1]),
Subcommand("list", Args["page", int, 1]),
)
)


async def get_average_price(item_data: ItemStack, session: AsyncSession) -> float:
result = await session.get(SellLog, {"item_namespace": str(item_data.item.getLocation())})
if result is not None:
return round(result.price_sum / result.sold_count, 2)
if (p := item_data.getNbt("price")) is not None:
return p
raise TypeError


@matcher.assign("$main")
async def handle_market_main(user_id: str = get_user_id()) -> None:
async with get_session() as session:
count = len((await session.scalars(select(MarketItem).where(MarketItem.remain_count > 0))).all())
await lang.finish("main.info", user_id, count)


@matcher.assign("sell")
async def handle_market_sell(bag_index: int, count: int, price_diff: str, user_id: str = get_user_id()) -> None:
try:
item = await get_bag_item(user_id, bag_index)
except IndexError:
await lang.finish("sell.index_error", user_id)
if count < 0:
await lang.finish("sell.wrong_count", user_id)
elif count == 0:
count = item.stack.count
elif count > item.stack.count:
await lang.finish("sell.item_not_enough", user_id, item.stack.count)
async with get_session() as session:
try:
avg_price = await get_average_price(item.stack, session)
except TypeError:
await lang.finish("sell.not_soldable", user_id)
if price_diff.startswith("+"):
price = round(avg_price * (1 + 0.01 * min(5, len(price_diff))), 2)
elif price_diff.startswith("-"):
price = round(avg_price * (1 - 0.01 * min(5, len(price_diff))), 2)
else:
price = avg_price
session.add(
MarketItem(
user_id=user_id,
remain_count=count,
item_data=json.dumps(item.stack.data).encode("utf-8"),
price=price,
item_namespace=str(item.stack.item.getLocation()),
)
)
await session.commit()
item.stack.count -= count
await lang.finish("sell.done", user_id, count, item.stack.getName(), price)


async def get_market_item(user_id: str, data: MarketItem) -> ItemStack:
location = get_location_by_id(data.item_namespace)
return await get_item(location, user_id, data.remain_count, json.loads(data.item_data))


async def get_market_items_by_name(user_id: str, session: AsyncSession, name: str) -> list[MarketItem]:
items = []
for item_data in await session.scalars(select(MarketItem).where(MarketItem.remain_count > 0)):
item = await get_market_item(user_id, item_data)
if name in await item.getName():
items.append(item_data)
return items


async def give_market_item(count: int, item_data: MarketItem, user_id: str) -> None:
item = await get_market_item(user_id, item_data)
item.count = count
await give_item(user_id, item)


@matcher.assign("buy")
async def handle_market_buy(name: str, count: int, user_id: str = get_user_id()) -> None:
bought_count = 0
used_vimcoin = 0
user = await get_user(user_id)
async with get_session() as session:
items = await get_market_items_by_name(user_id, session, name)
for item in items:
if item.remain_count >= (c := count - bought_count) and await user.has_vimcoin(p := item.price * c):
bought_count = count
await user.use_vimcoin(p, True)
await (await get_user(item.user_id)).add_vimcoin(p * 0.99)
await give_market_item(c, item, user_id)
item.remain_count -= c
used_vimcoin += p
break
elif await user.has_vimcoin(p := item.price * item.remain_count):
bought_count += item.remain_count
await user.use_vimcoin(p, True)
await (await get_user(item.user_id)).add_vimcoin(p * 0.99)
await give_market_item(item.remain_count, item, user_id)
await session.delete(item)
used_vimcoin += p
await session.commit()
await lang.finish("buy.finish", user_id, bought_count, name, used_vimcoin)


ITEMS_PER_PAGE = 10


@matcher.assign("list")
async def handle_market_list(page: int, user_id: str = get_user_id()) -> None:
if page < 1:
page = 1
async with get_session() as session:
query = select(MarketItem).where(MarketItem.remain_count > 0).order_by(MarketItem.item_id.desc())
all_items = (await session.scalars(query)).all()
total = len(all_items)
if total == 0:
await lang.finish("list.empty", user_id)
total_pages = (total + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
if page > total_pages:
page = total_pages
start = (page - 1) * ITEMS_PER_PAGE
page_items = all_items[start : start + ITEMS_PER_PAGE]
lines = []
for idx, market_item in enumerate(page_items, start=start + 1):
item = await get_market_item(user_id, market_item)
name = await item.getName()
lines.append(await lang.text("list.item_line", user_id, idx, name, market_item.remain_count, market_item.price))
items_text = "\n".join(lines)
await lang.finish("list.header", user_id, page, total_pages, total, items_text)
12 changes: 12 additions & 0 deletions src/plugins/nonebot_plugin_market/help.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugin: market
commands:
market:
description: help.description
details: help.details
usages:
- help.usage1
- help.usage2
- help.usage3
- help.usage4
- help.usage5
category: game
18 changes: 18 additions & 0 deletions src/plugins/nonebot_plugin_market/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from nonebot_plugin_orm import Model
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column


class MarketItem(Model):
item_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
item_namespace: Mapped[str] = mapped_column(String(64))
remain_count: Mapped[int]
price: Mapped[float]
user_id: Mapped[str] = mapped_column(String(128))
item_data: Mapped[bytes]


class SellLog(Model):
item_namespace: Mapped[str] = mapped_column(String(64), primary_key=True)
sold_count: Mapped[int]
price_sum: Mapped[float]
1 change: 1 addition & 0 deletions src/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ packages = [
{ include = "nonebot_plugin_hkrpg_calendar", from = "./plugins" },
{ include = "nonebot_plugin_ba_calendar", from = "./plugins" },
{ include = "nonebot_plugin_everyday_wife", from = "./plugins" },
{ include = "nonebot_plugin_market", from = "./plugins" },
{ include = "nonebot_plugin_online_timer", from = "./plugins" },
{ include = "nonebot_plugin_status_report", from = "./plugins" },
{ include = "nonebot_plugin_broadcast", from = "./plugins" },
Expand Down