├── .gitignore ├── README.md ├── niuniu ├── __init__.py ├── config.py ├── data_source.py ├── database.py ├── fence.py ├── handler.py ├── meiboli.png ├── menghanyao.png ├── model.py ├── niuniu.py ├── niuniu_goods │ ├── __init__.py │ ├── event_manager.py │ ├── events.yaml │ ├── goods.py │ └── model.py ├── shop.py ├── templates │ ├── my_info.html │ └── record_info.html ├── utils.py ├── weige.png └── yuban.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > 3 | > **寻求合作者** 4 | > 5 | > 因学业繁忙,维护此插件较为困难,现寻求合作者一同维护 6 | 7 | # zhenxun_plugin_niuniu 8 | 真寻群内小游戏插件-牛牛大作战 9 | 10 | 11 | ## 使用方法 12 | 1. 下载压缩包,解压并放入`zhenxun/plugins`文件夹或其他自定义文件夹中 13 | 2. 对Bot发送`添加插件 niuniu` 14 | 15 | ## 指令 16 | |指令|功能描述| 17 | |---|---| 18 | |注册牛牛|注册你的牛牛| 19 | |注销牛牛|删除你的牛牛| 20 | |jj/击剑 @user|与注册牛牛的人进行击剑,对战结果影响牛牛长度| 21 | |我的牛牛|查看自己牛牛长度| 22 | |我的牛牛战绩|查看自己牛牛战绩| 23 | |牛牛长度排行|查看本群正数牛牛长度排行| 24 | |牛牛长度总排行|查看正数牛牛长度排行总榜| 25 | |牛牛深度排行|查看本群负数牛牛深度排行| 26 | |牛牛深度总排行|查看负数牛牛深度排行总榜| 27 | |打胶|对自己的牛牛进行操作,结果随机| 28 | -------------------------------------------------------------------------------- /niuniu/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from nonebot.plugin import PluginMetadata 4 | 5 | from zhenxun.configs.path_config import IMAGE_PATH 6 | from zhenxun.configs.utils import PluginExtraData 7 | from zhenxun.services.log import logger 8 | from zhenxun.services.plugin_init import PluginInit 9 | 10 | from .config import ICON_PATH 11 | from .handler import ( 12 | niuniu_deep_rank, # noqa: F401 13 | niuniu_deep_rank_all, # noqa: F401 14 | niuniu_fencing, # noqa: F401 15 | niuniu_hit_glue, # noqa: F401 16 | niuniu_length_rank, # noqa: F401 17 | niuniu_length_rank_all, # noqa: F401 18 | niuniu_my, # noqa: F401 19 | niuniu_my_record, # noqa: F401 20 | niuniu_register, # noqa: F401 21 | niuniu_unsubscribe, # noqa: F401 22 | ) 23 | from .shop import * # noqa: F403 24 | 25 | __plugin_meta__ = PluginMetadata( 26 | name="牛牛大作战", 27 | description="牛牛大作战,男同快乐游", 28 | usage=""" 29 | 牛牛大作战,男同快乐游 30 | 合理安排时间,享受健康生活 31 | 32 | 注册牛牛 --注册你的牛牛 33 | 注销牛牛 --销毁你的牛牛(花费500金币) 34 | 击剑/jj [@user] --与注册牛牛的人进行击剑,对战结果影响牛牛长度 35 | 我的牛牛 --查看自己牛牛长度 36 | 我的牛牛战绩 --查看自己牛牛战绩 37 | 牛牛长度排行 --查看本群正数牛牛长度排行 38 | 牛牛深度排行 --查看本群负数牛牛深度排行 39 | 牛牛长度总排行 --查看正数牛牛长度排行总榜 40 | 牛牛深度总排行 --查看负数牛牛深度排行总榜 41 | 打胶 --对自己的牛牛进行操作,结果随机 42 | """.strip(), 43 | extra=PluginExtraData( 44 | author="molanp", 45 | version="1.2.rc3", 46 | menu_type="群内小游戏", 47 | ).dict(), 48 | ) 49 | 50 | 51 | RESOURCE_FILES = [ 52 | IMAGE_PATH / "shop_icon" / "weige.png", 53 | IMAGE_PATH / "shop_icon" / "meiboli.png", 54 | IMAGE_PATH / "shop_icon" / "menghanyao.png", 55 | IMAGE_PATH / "shop_icon" / "yuban.png", 56 | ] 57 | 58 | GOOD_FILES = [ 59 | ICON_PATH / "weige.png", 60 | ICON_PATH / "meiboli.png", 61 | ICON_PATH / "menghanyao.png", 62 | ICON_PATH / "yuban.png" 63 | ] 64 | 65 | 66 | class MyPluginInit(PluginInit): 67 | async def install(self): 68 | for res_file in RESOURCE_FILES + GOOD_FILES: 69 | res = Path(__file__).parent / res_file.name 70 | if res.exists(): 71 | if res_file.exists(): 72 | res_file.unlink() 73 | res.rename(res_file) 74 | logger.info(f"更新 NIUNIU 资源文件成功 {res} -> {res_file}") 75 | 76 | async def remove(self): 77 | for res_file in RESOURCE_FILES + GOOD_FILES: 78 | if res_file.exists(): 79 | res_file.unlink() 80 | logger.info(f"删除 NIUNIU 资源文件成功 {res_file}") 81 | -------------------------------------------------------------------------------- /niuniu/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | FENCE_COOLDOWN = 180 4 | """发起者冷却""" 5 | FENCED_PROTECTION = 300 6 | """ 被击剑者冷却保护 """ 7 | UNSUBSCRIBE_GOLD = 500 8 | """ 注销牛牛所需金币 """ 9 | QUICK_GLUE_COOLDOWN = 240 10 | """ 连续打胶冷却判定, 要比打胶冷却大,不然会出问题 """ 11 | GLUE_COOLDOWN = 180 12 | """ 打胶冷却时间 """ 13 | ICON_PATH = Path(__file__).parent 14 | """商店目录""" 15 | -------------------------------------------------------------------------------- /niuniu/data_source.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from nonebot import get_bot, require 4 | require("nonebot_plugin_uninfo") 5 | from nonebot_plugin_uninfo import Uninfo 6 | from tortoise.functions import Max, Min 7 | 8 | from zhenxun.services.log import logger 9 | from zhenxun.utils.image_utils import BuildImage, ImageTemplate 10 | from zhenxun.utils.platform import PlatformUtils 11 | 12 | from .model import NiuNiuRecord, NiuNiuUser 13 | 14 | 15 | class NiuNiuQuick: 16 | @classmethod 17 | async def get_length(cls, uid: int | str) -> float | None: 18 | user = await NiuNiuUser.get_or_none(uid=uid).values("length") 19 | return user["length"] if user else None 20 | 21 | @classmethod 22 | async def random_length(cls) -> float: 23 | users = await NiuNiuUser.all().order_by("length").values("length") 24 | 25 | if not users: 26 | origin_length = 10.0 27 | else: 28 | length_values = [u["length"] for u in users] 29 | index = min(int(len(length_values) * 0.3), len(length_values) - 1) 30 | origin_length = float(length_values[index]) 31 | 32 | return round(origin_length * 0.9, 2) 33 | 34 | @classmethod 35 | async def latest_gluing_time(cls, uid: int) -> str: 36 | record = ( 37 | await NiuNiuRecord.filter(uid=uid, action="gluing") 38 | .order_by("-time") 39 | .first() 40 | .values("time") 41 | ) 42 | 43 | return record["time"] if record else "暂无记录" 44 | 45 | @classmethod 46 | async def get_nearest_lengths(cls, target_length: float) -> list[float]: 47 | # 使用ORM聚合查询 48 | greater_length = ( 49 | await NiuNiuUser.filter(length__gt=target_length) 50 | .annotate(min_length=Min("length")) 51 | .values("min_length") 52 | ) 53 | 54 | less_length = ( 55 | await NiuNiuUser.filter(length__lt=target_length) 56 | .annotate(max_length=Max("length")) 57 | .values("max_length") 58 | ) 59 | 60 | return [ 61 | greater_length[0]["min_length"] if greater_length else 0, 62 | less_length[0]["max_length"] if less_length else 0, 63 | ] 64 | 65 | @classmethod 66 | async def gluing(cls, origin_length: float) -> tuple[float, float]: 67 | result = await cls.get_nearest_lengths(origin_length) 68 | if result[0] != 0 or result[1] != 0: 69 | new_length = origin_length + result[0] * 0.3 - result[1] * 0.6 70 | return round(new_length, 2), round(new_length - origin_length, 2) 71 | 72 | if origin_length <= 0: 73 | prob = random.choice([-1.1, -1, -1, -1, -1, 1, 1, 1, 1]) 74 | diff = prob * 0.1 * origin_length + 1 75 | else: 76 | prob = random.choice([1, 1, 1, 1, 1, 0.9, -1, -1, -1, -1, -1, -1.4]) 77 | diff = prob * 0.1 * origin_length - 1 78 | new_length = origin_length + diff 79 | return round(new_length, 2), round(new_length - origin_length, 2) 80 | 81 | @classmethod 82 | async def comment(cls, length: float) -> str: 83 | if length <= -100: 84 | return ( 85 | "哇哦!你已经进化成魅魔了!" 86 | "魅魔在击剑时有20%的几率消耗自身长度吞噬对方牛牛呢!" 87 | ) 88 | elif -100 < length <= -50: 89 | return "嗯……好像已经穿过了身体吧……从另一面来看也可以算是凸出来的吧?" 90 | elif -50 < length <= -25: 91 | return random.choice( 92 | [ 93 | "这名女生,你的身体很健康哦!", 94 | "WOW,真的凹进去了好多呢!", 95 | "你已经是我们女孩子的一员啦!", 96 | ] 97 | ) 98 | elif -25 < length <= -10: 99 | return random.choice( 100 | [ 101 | "你已经是一名女生了呢!", 102 | "从女生的角度来说,你发育良好哦!", 103 | "你醒啦?你已经是一名女孩子啦!", 104 | "唔……可以放进去一根手指了都……", 105 | ] 106 | ) 107 | elif -10 < length <= 0: 108 | return random.choice( 109 | [ 110 | "安了安了,不要伤心嘛,做女生有什么不好的啊.", 111 | "不哭不哭,摸摸头,虽然很难再长出来,但是请不要伤心啦啊!", 112 | "加油加油!我看好你哦!", 113 | "你醒啦?你现在已经是一名女孩子啦!", 114 | "成为香香软软的女孩子吧!", 115 | ] 116 | ) 117 | elif 0 < length <= 10: 118 | return random.choice( 119 | [ 120 | "你行不行啊?细狗!", 121 | "虽然短,但是小小的也很可爱呢.", 122 | "像一只蚕宝宝.", 123 | "长大了.", 124 | ] 125 | ) 126 | elif 10 < length <= 25: 127 | return random.choice( 128 | [ 129 | "唔……没话说", 130 | "已经很长了呢!", 131 | ] 132 | ) 133 | elif 25 < length <= 50: 134 | return random.choice( 135 | [ 136 | "话说这种真的有可能吗?", 137 | "厚礼谢!", 138 | ] 139 | ) 140 | elif 50 < length <= 100: 141 | return random.choice( 142 | [ 143 | "已经突破天际了嘛……", 144 | "唔……这玩意应该不会变得比我高吧?", 145 | "你这个长度会死人的……!", 146 | "你马上要进化成牛头人了!!", 147 | "你是什么怪物,不要过来啊!!", 148 | ] 149 | ) 150 | else: 151 | return ( 152 | "惊世骇俗!你已经进化成牛头人了!" 153 | "牛头人在击剑时有20%的几率消耗自身长度吞噬对方牛牛呢!" 154 | ) 155 | 156 | @classmethod 157 | async def rank( 158 | cls, num: int, session: Uninfo, deep: bool = False, is_all: bool = False 159 | ) -> BuildImage | str: 160 | data_list = [] 161 | order = "length" if deep else "-length" 162 | 163 | # 构建基础查询 164 | query = NiuNiuUser.all().order_by(order) 165 | uid2name = {} 166 | bot = get_bot(self_id=session.self_id) 167 | if not is_all and session.group: 168 | try: 169 | group_members = await bot.get_group_member_list( 170 | group_id=session.group.id 171 | ) 172 | user_ids = [ 173 | int(member["user_id"]) for member in group_members 174 | ] # 修复4: 统一为int类型 175 | query = query.filter(uid__in=user_ids) 176 | uid2name = { 177 | int(member["user_id"]): member["nickname"] 178 | for member in group_members 179 | } 180 | except Exception as e: 181 | logger.error("获取群成员失败", "niuniu", e=e) 182 | return f"获取群成员失败: {e!s}" 183 | 184 | # 执行查询并转换数据 185 | users = await query.limit(num).values("uid", "length") 186 | if not users: 187 | return "当前还没有人有牛牛哦..." 188 | 189 | # 修复5: 直接使用ORM查询结果 190 | user_id_list = [user["uid"] for user in users] 191 | index = ( 192 | user_id_list.index(int(session.user.id)) + 1 193 | if int(session.user.id) in user_id_list 194 | else "-1(未统计)" 195 | ) 196 | 197 | # 构建表格数据 198 | for i, user in enumerate(users): 199 | uid = user["uid"] 200 | length = user["length"] 201 | 202 | # 获取用户头像 203 | avatar_bytes = await PlatformUtils.get_user_avatar( 204 | str(uid), "qq", session.self_id 205 | ) 206 | 207 | # 获取用户昵称 208 | nickname = ( 209 | uid2name.get(uid) 210 | or (await bot.get_stranger_info(user_id=uid))["nickname"] 211 | ) 212 | 213 | data_list.append([f"{i + 1}", (avatar_bytes, 30, 30), nickname, length]) 214 | 215 | # 生成标题 216 | title_type = "深度" if deep else "长度" 217 | scope = "群组内" if session.group else "全局" 218 | title = f"{title_type}{scope}排行" 219 | tip = f"你的排名在{scope}第 {index} 位哦!" 220 | 221 | return await ImageTemplate.table_page( 222 | head_text=title, 223 | tip_text=tip, 224 | column_name=["排名", "头像", "名称", "长度"], 225 | data_list=data_list, 226 | ) 227 | -------------------------------------------------------------------------------- /niuniu/database.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | import aiosqlite 4 | 5 | from zhenxun.configs.path_config import DATA_PATH 6 | from zhenxun.services.log import logger 7 | 8 | from .model import NiuNiuRecord, NiuNiuUser 9 | 10 | 11 | class Sqlite: 12 | @classmethod 13 | async def sqlite2db(cls) -> None: 14 | cls.conn = await aiosqlite.connect(DATA_PATH / "niuniu" / "data.db") 15 | await NiuNiuUser.migrate_from_sqlite() 16 | await NiuNiuRecord.migrate_from_sqlite() 17 | await Sqlite.conn.close() 18 | logger.info("Sqlite数据库迁移完成", "niuniu") 19 | shutil.rmtree(DATA_PATH / "niuniu") 20 | 21 | @classmethod 22 | async def json2db(cls, file_data): 23 | """迁移JSON数据到ORM""" 24 | from tortoise.transactions import in_transaction 25 | 26 | async with in_transaction(): 27 | await NiuNiuUser.all().delete() 28 | users = [ 29 | NiuNiuUser( 30 | uid=str(user_id), 31 | length=user_length, 32 | sex="boy" if user_length > 0 else "girl", 33 | ) 34 | for group in file_data 35 | for user_id, user_length in group.items() 36 | ] 37 | await NiuNiuUser.bulk_create(users) 38 | logger.info("JSON数据迁移完成", "niuniu") 39 | 40 | @classmethod 41 | async def query( 42 | cls, 43 | table: str, 44 | columns: list | None = None, 45 | conditions: dict | None = None, 46 | order_by: str | None = None, 47 | limit: int | None = None, 48 | ) -> list[dict]: 49 | """ 50 | 根据条件查询数据。 51 | 52 | :param table: 要查询的表名。 53 | :param columns: 要查询的列名列表,如果不指定则查询所有列。 54 | :param conditions: 查询条件,字典格式,键为字段名,值为条件值。 55 | :param order_by: 排序条件,例如 "time DESC"。 56 | :param limit: 限制结果数量。 57 | :return: 查询结果的字典列表。 58 | """ 59 | columns_str = ", ".join(columns) if columns else "*" 60 | query = f"SELECT {columns_str} FROM {table}" 61 | 62 | if conditions: 63 | query += " WHERE " + " AND ".join( 64 | [f"{key} = ?" for key in conditions.keys()] 65 | ) 66 | 67 | if order_by: 68 | query += f" ORDER BY {order_by}" 69 | 70 | if limit is not None: 71 | query += f" LIMIT {limit}" 72 | 73 | async with cls.conn.cursor() as cursor: 74 | await cursor.execute( 75 | query, tuple(conditions.values()) if conditions else () 76 | ) 77 | result = await cursor.fetchall() 78 | if not result: 79 | return [] 80 | column_names = [description[0] for description in cursor.description] 81 | return [dict(zip(column_names, row)) for row in result] 82 | -------------------------------------------------------------------------------- /niuniu/fence.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from nonebot import require 4 | require("nonebot_plugin_uninfo") 5 | from nonebot_plugin_uninfo import Uninfo 6 | 7 | from zhenxun.configs.config import BotConfig 8 | from zhenxun.models.sign_user import SignUser 9 | 10 | from .model import NiuNiuUser 11 | from .niuniu import NiuNiu 12 | from .utils import UserState 13 | from .niuniu_goods.event_manager import get_buffs 14 | 15 | 16 | class Fencing: 17 | @classmethod 18 | async def fence(cls, rd): 19 | """ 20 | 根据比例减少/增加牛牛长度 21 | Args: 22 | rd (float, int): 随机数 23 | Returns: 24 | float: 四舍五入后的结果 25 | """ 26 | rd -= time.localtime().tm_sec % 10 27 | return round(abs(rd + random.uniform(0.13, 0.24) * rd) * 0.3, 2) 28 | 29 | @classmethod 30 | async def fencing(cls, my_length, oppo_length, at_qq, my_qq) -> str: 31 | """ 32 | 确定击剑比赛的结果。 33 | 34 | Args: 35 | my_length (float): 我的当前长度 36 | oppo_length (float): 对手的当前长度 37 | at_qq (str): 被 @ 的人的 QQ 号码。 38 | my_qq (str): 我的 QQ 号码。 39 | """ 40 | origin_my = my_length 41 | origin_oppo = oppo_length 42 | # 获取用户当前道具的击剑加成 43 | my_buff = await get_buffs(my_qq) 44 | fencing_weight = my_buff.fencing_weight 45 | 46 | # 传递到胜率计算 47 | win_probability = await cls.calculate_win_probability( 48 | my_length, oppo_length, fencing_weight 49 | ) 50 | 51 | # 根据胜率决定胜负 52 | result = random.choices( 53 | ["win", "lose"], weights=[win_probability, 1 - win_probability], k=1 54 | )[0] 55 | 56 | if result == "win": 57 | result, my_length, oppo_length = await cls.apply_skill( 58 | my_length, oppo_length, True, my_qq 59 | ) 60 | else: 61 | result, my_length, oppo_length = await cls.apply_skill( 62 | my_length, oppo_length, False, at_qq 63 | ) 64 | 65 | # 更新数据并返回结果 66 | await cls.update_data( 67 | { 68 | "my_qq": my_qq, 69 | "at_qq": at_qq, 70 | "new_my": my_length, 71 | "new_oppo": oppo_length, 72 | "origin_my": origin_my, 73 | "origin_oppo": origin_oppo, 74 | } 75 | ) 76 | return result 77 | 78 | @classmethod 79 | async def calculate_win_probability( 80 | cls, height_a, height_b, fencing_weight=1.0, min_win=0.05, max_win=0.85 81 | ): 82 | # 选手 A 的初始胜率 83 | p_a = 0.85 * fencing_weight 84 | 85 | # 计算长度比例,考虑允许负数(取绝对值比较大小) 86 | height_ratio = max(abs(height_a), abs(height_b)) / min( 87 | abs(height_a), abs(height_b) 88 | ) 89 | 90 | # 根据长度比例计算胜率减少率 91 | reduction_rate = 0.1 * (height_ratio - 1) 92 | 93 | # 计算 A 的胜率减少量 94 | reduction = p_a * reduction_rate 95 | 96 | # 调整 A 的胜率 97 | adjusted_p_a = p_a - reduction 98 | 99 | # 如果 height_a 为负,则反转胜率方向(负数表示对抗优势减弱) 100 | if height_a < 0: 101 | adjusted_p_a = 1.0 - adjusted_p_a 102 | 103 | return max(min_win, min(adjusted_p_a, max_win)) 104 | 105 | @classmethod 106 | async def apply_skill( 107 | cls, my, oppo, increase_length, uid 108 | ) -> tuple[str, float, float]: 109 | """ 110 | 应用击剑技巧并生成结果字符串。 111 | 112 | Args: 113 | my (float): 长度1。 114 | oppo (float): 长度2。 115 | increase_length (bool): 是否增加长度。 116 | uid (str): 用户 ID。 117 | """ 118 | base_change = min(abs(my), abs(oppo)) * 0.1 # 基于较小值计算变化量 119 | reduce = await cls.fence(base_change) # 传入基础变化量 120 | reduce *= await NiuNiu.apply_decay(1) # 🚨 全局衰减系数 121 | 122 | # 添加动态平衡系数 123 | balance_factor = 1 - abs(my - oppo) / 100 # 差距越大变化越小 124 | reduce *= max(0.3, balance_factor) 125 | 126 | # 获取用户 Buff 效果 127 | buff = await get_buffs(uid) 128 | if buff: 129 | reduce *= buff.glue_effect 130 | reduce = round(reduce, 2) 131 | 132 | if increase_length: 133 | my += reduce 134 | oppo -= 0.8 * reduce 135 | if my < 0: 136 | result = random.choice( 137 | [ 138 | f"哦吼!?你的牛牛在长大欸!长大了{reduce}cm!", 139 | f"牛牛凹进去的深度变浅了欸!变浅了{reduce}cm!", 140 | ] 141 | ) 142 | else: 143 | result = ( 144 | f"你以绝对的长度让对方屈服了呢!你的长度增加{reduce}cm," 145 | f"对方减少了{round(0.8 * reduce, 2)}cm!" 146 | f"你当前长度为{round(my, 2)}cm!" 147 | ) 148 | else: 149 | my -= reduce 150 | oppo += 0.8 * reduce 151 | if my < 0: 152 | result = random.choice( 153 | [ 154 | f"哦吼!?看来你的牛牛因为击剑而凹进去了呢🤣🤣🤣!凹进去了{reduce}cm!", 155 | f"由于对方击剑技术过于高超,造成你的牛牛凹了进去呢😰!凹进去了{reduce}cm!", 156 | f"好惨啊,本来就不长的牛牛现在凹进去了呢😂!凹进去了{reduce}cm!", 157 | ] 158 | ) 159 | else: 160 | result = ( 161 | f"对方以绝对的长度让你屈服了呢!你的长度减少{reduce}cm," 162 | f"当前长度{round(my, 2)}cm!" 163 | ) 164 | return result, my, oppo 165 | 166 | @classmethod 167 | async def update_data(cls, data: dict): 168 | """ 169 | 更新数据 170 | 171 | Args: 172 | data (dict): 数据 173 | """ 174 | my_qq = data["my_qq"] 175 | at_qq = data["at_qq"] 176 | new_my = round(data["new_my"], 2) 177 | new_oppo = round(data["new_oppo"], 2) 178 | origin_my = round(data["origin_my"], 2) 179 | origin_oppo = round(data["origin_oppo"], 2) 180 | await NiuNiuUser.filter(uid=my_qq).update(length=new_my) 181 | await NiuNiu.record_length(my_qq, origin_my, new_my, "fencing") 182 | await NiuNiuUser.filter(uid=at_qq).update(length=new_oppo) 183 | await NiuNiu.record_length(at_qq, origin_oppo, new_oppo, "fenced") 184 | 185 | @classmethod 186 | async def with_bot(cls, session: Uninfo, user_id: str) -> str: 187 | """ 188 | 获取 bot 实例 189 | 190 | Args: 191 | session (Uninfo): 会话对象 192 | user_id (str): 用户 ID 193 | 194 | Returns: 195 | str: 击剑结果 196 | """ 197 | bot = await NiuNiu.get_length(session.self_id) 198 | user = await NiuNiu.get_length(user_id) 199 | assert user is not None 200 | if bot is not None: 201 | """Bot不应该参与排行榜统计""" 202 | await NiuNiuUser.filter(uid=session.self_id).delete() 203 | sign_user = await SignUser.get_or_none(user_id=user_id) 204 | impression = 0 if sign_user is None else sign_user.impression 205 | if impression >= 50: 206 | _, new_user, _ = await cls.apply_skill(user, 0, True, user_id) 207 | r = random.choice( 208 | [ 209 | "{nickname}喜欢你,你的长度增加了{diff}cm呢!", 210 | ] 211 | ) 212 | else: 213 | _, new_user, _ = await cls.apply_skill(user, 0, False, user_id) 214 | r = random.choice( 215 | [ 216 | "你弄疼{nickname}了,给你头咬掉,牛牛长度变短{diff}cm", 217 | "{nickname}感到恶心,脚踩了你的牛牛,牛牛长度变短{diff}cm", 218 | "你被{nickname}的牛牛戳到了,牛牛长度变短{diff}cm", 219 | "{nickname}偷偷给你下了药,你的牛牛长度变短了{diff}cm", 220 | ] 221 | ) 222 | await NiuNiuUser.filter(uid=user_id).update(length=round(new_user)) 223 | await NiuNiu.record_length(user_id, round(user), round(new_user), "fencing") 224 | await UserState.update("fence_time_map", user_id, time.time() + random.randrange(300, 600)) 225 | return r.format( 226 | nickname=BotConfig.self_nickname, 227 | diff=abs(round(new_user - user)), 228 | ) 229 | -------------------------------------------------------------------------------- /niuniu/handler.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import contextlib 3 | from pathlib import Path 4 | import random 5 | import time 6 | 7 | import aiofiles 8 | from nonebot import get_driver, require 9 | require("nonebot_plugin_alconna") 10 | require("nonebot_plugin_uninfo") 11 | from nonebot_plugin_alconna import ( 12 | Alconna, 13 | Args, 14 | Arparma, 15 | At, 16 | Image, 17 | MultiVar, 18 | Text, 19 | on_alconna, 20 | ) 21 | from nonebot_plugin_htmlrender import template_to_pic 22 | from nonebot_plugin_uninfo import Uninfo 23 | from tortoise.exceptions import DoesNotExist 24 | 25 | from zhenxun.configs.path_config import DATA_PATH 26 | from zhenxun.models.user_console import UserConsole 27 | from zhenxun.utils.enum import GoldHandle 28 | from zhenxun.utils.message import MessageUtils 29 | from zhenxun.utils.platform import PlatformUtils 30 | 31 | from .config import ( 32 | FENCE_COOLDOWN, 33 | FENCED_PROTECTION, 34 | GLUE_COOLDOWN, 35 | QUICK_GLUE_COOLDOWN, 36 | UNSUBSCRIBE_GOLD, 37 | ) 38 | from .database import Sqlite 39 | from .fence import Fencing 40 | from .model import NiuNiuUser 41 | from .niuniu import NiuNiu 42 | from .niuniu_goods.event_manager import process_glue_event, get_buffs 43 | from .utils import UserState, get_name 44 | 45 | niuniu_register = on_alconna( 46 | Alconna("注册牛牛"), 47 | priority=5, 48 | block=True, 49 | ) 50 | niuniu_unsubscribe = on_alconna( 51 | Alconna("注销牛牛"), 52 | priority=5, 53 | block=True, 54 | ) 55 | niuniu_fencing = on_alconna( 56 | Alconna("击剑", Args["targets?", MultiVar(At), []]), 57 | aliases=("JJ", "Jj", "jJ", "jj"), 58 | priority=5, 59 | block=True, 60 | ) 61 | niuniu_my = on_alconna( 62 | Alconna("我的牛牛"), 63 | priority=5, 64 | block=True, 65 | ) 66 | niuniu_length_rank = on_alconna( 67 | Alconna("牛牛长度排行", Args["num?", int, 10]), 68 | priority=5, 69 | block=True, 70 | ) 71 | niuniu_deep_rank = on_alconna( 72 | Alconna("牛牛深度排行", Args["num?", int, 10]), 73 | priority=5, 74 | block=True, 75 | ) 76 | niuniu_length_rank_all = on_alconna( 77 | Alconna("牛牛长度总排行", Args["num?", int, 10]), 78 | priority=5, 79 | block=True, 80 | ) 81 | niuniu_deep_rank_all = on_alconna( 82 | Alconna("牛牛深度总排行", Args["num?", int, 10]), 83 | priority=5, 84 | block=True, 85 | ) 86 | niuniu_hit_glue = on_alconna( 87 | Alconna("打胶"), 88 | priority=5, 89 | block=True, 90 | ) 91 | 92 | niuniu_my_record = on_alconna( 93 | Alconna("我的牛牛战绩", Args["num?", int, 10]), 94 | priority=5, 95 | block=True, 96 | ) 97 | 98 | 99 | driver = get_driver() 100 | 101 | 102 | @driver.on_startup 103 | async def start(): 104 | old_data_path = Path(__file__).resolve().parent / "data" / "long.json" 105 | if old_data_path.exists(): 106 | async with aiofiles.open(old_data_path, encoding="utf-8") as f: 107 | file_data = f.read() 108 | await Sqlite.json2db(file_data) 109 | old_data_path.unlink() 110 | if Path(DATA_PATH / "niuniu" / "data.db").exists(): 111 | await Sqlite.sqlite2db() 112 | return 113 | 114 | 115 | @niuniu_register.handle() 116 | async def _(session: Uninfo): 117 | uid = str(session.user.id) 118 | if await NiuNiuUser.filter(uid=uid).exists(): 119 | await niuniu_register.send(Text("你已经有过牛牛啦!"), reply_to=True) 120 | return 121 | length = await NiuNiu.random_length() 122 | await NiuNiuUser.create(uid=uid, length=length) 123 | await NiuNiu.record_length(uid, 0, length, "register") 124 | if length > 0: 125 | await niuniu_register.send( 126 | Text(f"牛牛长出来啦!足足有{length}cm呢"), reply_to=True 127 | ) 128 | else: 129 | await niuniu_register.send( 130 | Text( 131 | f"牛牛长出来了?牛牛不见了!你是个可爱的女孩纸!!深度足足有{abs(length)}cm呢!" 132 | ), 133 | reply_to=True, 134 | ) 135 | 136 | 137 | @niuniu_unsubscribe.handle() 138 | async def _(session: Uninfo): 139 | uid = session.user.id 140 | length = await NiuNiu.get_length(uid) 141 | if length is None: 142 | await niuniu_unsubscribe.send( 143 | Text("你还没有牛牛呢!\n请发送'注册牛牛'领取你的牛牛!"), reply_to=True 144 | ) 145 | return 146 | gold = (await UserConsole.get_user(uid)).gold 147 | if gold < UNSUBSCRIBE_GOLD: 148 | await niuniu_unsubscribe.send( 149 | Text(f"你的金币不足{UNSUBSCRIBE_GOLD},无法注销牛牛!"), reply_to=True 150 | ) 151 | else: 152 | await UserConsole.reduce_gold( 153 | uid, UNSUBSCRIBE_GOLD, GoldHandle.PLUGIN, "niuniu" 154 | ) 155 | await NiuNiuUser.filter(uid=uid).delete() 156 | await NiuNiu.record_length(uid, length, 0, "unsubscribe") 157 | await niuniu_unsubscribe.finish(Text("从今往后你就没有牛牛啦!"), reply_to=True) 158 | 159 | 160 | @niuniu_fencing.handle() 161 | async def _(session: Uninfo, p: Arparma): 162 | at_list = p.query("targets") 163 | uid = session.user.id 164 | with contextlib.suppress(KeyError): 165 | next_time = await UserState.get("fence_time_map", uid) 166 | if next_time is None: 167 | raise KeyError 168 | if time.time() < next_time: 169 | time_rest = round(next_time - time.time(), 2) 170 | jj_refuse = [ 171 | f"不行不行,你的身体会受不了的,歇{time_rest}s再来吧", 172 | f"你这种男同就应该被送去集中营!等待{time_rest}s再来吧", 173 | f"打咩哟!你的牛牛会炸的,休息{time_rest}s再来吧", 174 | ] 175 | await niuniu_fencing.send(random.choice(jj_refuse), reply_message=True) 176 | return 177 | if not at_list: 178 | await niuniu_fencing.send("你要和谁击剑?你自己吗?", reply_message=True) 179 | return 180 | my_long = await NiuNiu.get_length(uid) 181 | try: 182 | if my_long is None: 183 | raise RuntimeError( 184 | "你还没有牛牛呢!不能击剑!\n请发送'注册牛牛'领取你的牛牛!" 185 | ) 186 | at = str(at_list[0].target) 187 | if len(at_list) >= 2: 188 | raise RuntimeError( 189 | random.choice( 190 | ["一战多?你的小身板扛得住吗?", "你不准参加Impart┗|`O′|┛"] 191 | ) 192 | ) 193 | if at == uid: 194 | raise RuntimeError("不能和自己击剑哦!") 195 | if at == session.self_id: 196 | r = await Fencing.with_bot(session, uid) 197 | raise RuntimeError(r) 198 | opponent_long = await NiuNiu.get_length(at) 199 | if opponent_long is None: 200 | raise RuntimeError("对方还没有牛牛呢!不能击剑!") 201 | # 被击剑者冷却检查 202 | next_fenced_time = await UserState.get("fenced_time_map", at, None) 203 | if next_fenced_time is None: 204 | next_fenced_time = (await NiuNiu.last_fenced_time(at)) + FENCED_PROTECTION 205 | now_fenced_time_user = time.time() 206 | rest = round(next_fenced_time - now_fenced_time_user, 2) 207 | if now_fenced_time_user < next_fenced_time: 208 | tips = [ 209 | f"对方刚被击剑过,需要休息{rest}秒才能再次被击剑", 210 | f"对方牛牛还在恢复中,{rest}秒后再来吧", 211 | f"禁止连续击剑同一用户!请{rest}秒后再来!", 212 | ] 213 | await niuniu_fencing.send(random.choice(tips), reply_message=True) 214 | return 215 | 216 | # 处理击剑逻辑 217 | result = await Fencing.fencing(my_long, opponent_long, at, uid) 218 | 219 | # 更新数据 220 | await UserState.update("fence_time_map", uid, time.time() + FENCE_COOLDOWN) 221 | await UserState.update("fenced_time_map", at, time.time() + FENCED_PROTECTION) 222 | await niuniu_fencing.send(result, reply_message=True) 223 | except RuntimeError as e: 224 | await niuniu_fencing.send(str(e), reply_message=True) 225 | 226 | 227 | @niuniu_my.handle() 228 | async def _(session: Uninfo): 229 | uid = session.user.id 230 | if await NiuNiu.get_length(uid) is None: 231 | await niuniu_my.send( 232 | Text("你还没有牛牛呢!\n请发送'注册牛牛'领取你的牛牛!"), reply_to=True 233 | ) 234 | return 235 | 236 | try: 237 | current_user = await NiuNiuUser.get(uid=uid) 238 | except DoesNotExist: 239 | await niuniu_my.send(Text("未查询到数据..."), reply_to=True) 240 | return 241 | 242 | # 计算排名逻辑 243 | rank = await NiuNiuUser.filter(length__gt=current_user.length).count() + 1 244 | 245 | # 构造结果数据 246 | user = {"uid": current_user.uid, "length": current_user.length, "rank": rank} 247 | avatar = await PlatformUtils.get_user_avatar(uid, "qq", session.self_id) 248 | avatar = "" if avatar is None else base64.b64encode(avatar).decode("utf-8") 249 | 250 | result = { 251 | "avatar": f"data:image/png;base64,{avatar}", 252 | "name": await get_name(session), 253 | "rank": user["rank"], 254 | "my_length": user["length"], 255 | "latest_gluing_time": await NiuNiu.latest_gluing_time(uid), 256 | "comment": await NiuNiu.comment(user["length"]), 257 | "buff": await get_buffs(uid), 258 | "now": time.time() 259 | } 260 | template_dir = Path(__file__).resolve().parent / "templates" 261 | pic = await template_to_pic( 262 | template_path=str(template_dir), 263 | template_name="my_info.html", 264 | templates=result, 265 | ) 266 | await niuniu_my.send(Image(raw=pic), reply_to=True) 267 | 268 | 269 | @niuniu_length_rank.handle() 270 | async def _(session: Uninfo, p: Arparma): 271 | num = p.query("num") 272 | assert isinstance(num, int) 273 | if num > 50: 274 | await niuniu_length_rank.finish(Text("排行榜人数不能超过50哦...")) 275 | gid = session.group.id if session.group else None 276 | if not gid: 277 | await niuniu_length_rank.finish( 278 | Text("私聊中无法查看 '牛牛长度排行',请发送 '牛牛长度总排行'") 279 | ) 280 | image = await NiuNiu.rank(num, session) 281 | await MessageUtils.build_message(image).send() 282 | 283 | 284 | @niuniu_length_rank_all.handle() 285 | async def _(session: Uninfo, p: Arparma): 286 | num = p.query("num") 287 | assert isinstance(num, int) 288 | if num > 50: 289 | await niuniu_length_rank_all.finish(Text("排行榜人数不能超过50哦...")) 290 | image = await NiuNiu.rank(num, session, is_all=True) 291 | await MessageUtils.build_message(image).send() 292 | 293 | 294 | @niuniu_deep_rank.handle() 295 | async def _(session: Uninfo, p: Arparma): 296 | num = p.query("num") 297 | assert isinstance(num, int) 298 | if num > 50: 299 | await niuniu_deep_rank.finish(Text("排行榜人数不能超过50哦...")) 300 | gid = session.group.id if session.group else None 301 | if not gid: 302 | await niuniu_deep_rank.finish( 303 | Text("私聊中无法查看 '牛牛深度排行',请发送 '牛牛深度总排行'") 304 | ) 305 | image = await NiuNiu.rank(num, session, True) 306 | await MessageUtils.build_message(image).send() 307 | 308 | 309 | @niuniu_deep_rank_all.handle() 310 | async def _(session: Uninfo, p: Arparma): 311 | num = p.query("num") 312 | assert isinstance(num, int) 313 | if num > 50: 314 | await niuniu_deep_rank_all.finish(Text("排行榜人数不能超过50哦...")) 315 | image = await NiuNiu.rank(num, session, True, is_all=True) 316 | await MessageUtils.build_message(image).send() 317 | 318 | 319 | @niuniu_hit_glue.handle() 320 | async def hit_glue(session: Uninfo): 321 | uid = session.user.id 322 | origin_length = await NiuNiu.get_length(uid) 323 | if origin_length is None: 324 | await niuniu_hit_glue.send( 325 | Text( 326 | random.choice( 327 | [ 328 | "你还没有牛牛呢!不能打胶!\n请发送'注册牛牛'", 329 | "无牛牛,打胶不要的!\n请发送'注册牛牛'", 330 | ] 331 | ) 332 | ), 333 | reply_to=True, 334 | ) 335 | return 336 | 337 | next_hit_glue_time = await UserState.get("gluing_time_map", uid, 0) 338 | glue_now_time = time.time() 339 | if glue_now_time < next_hit_glue_time: 340 | time_rest = round(next_hit_glue_time - glue_now_time, 2) 341 | glue_refuse = [ 342 | f"不行不行,你的身体会受不了的,歇{time_rest}s再来吧", 343 | f"休息一下吧,会炸膛的!{time_rest}s后再来吧", 344 | f"打咩哟,你的牛牛会爆炸的,休息{time_rest}s再来吧", 345 | ] 346 | await niuniu_hit_glue.finish(Text(random.choice(glue_refuse)), reply_to=True) 347 | is_rapid_glue = time.time() < QUICK_GLUE_COOLDOWN + next_hit_glue_time 348 | # 更新冷却时间 349 | await UserState.update("gluing_time_map", uid, time.time() + GLUE_COOLDOWN) 350 | 351 | # 处理事件 352 | result, new_length, _ = await process_glue_event(uid, origin_length, is_rapid_glue) 353 | 354 | # 更新数据 355 | await NiuNiu.update_length(uid, new_length) 356 | await NiuNiu.record_length(uid, origin_length, new_length, "gluing") 357 | 358 | # 发送结果 359 | await niuniu_hit_glue.send(Text(result), reply_to=True) 360 | 361 | 362 | @niuniu_my_record.handle() 363 | async def my_record(session: Uninfo, p: Arparma): 364 | uid = session.user.id 365 | num = p.query("num") 366 | assert isinstance(num, int) 367 | if num > 50: 368 | await niuniu_my_record.finish(Text("记录查看数不能超过50哦...")) 369 | records = await NiuNiu.get_user_records(uid, num) 370 | 371 | if not records: 372 | await niuniu_my_record.send(Text("你还没有任何牛牛战绩哦~"), reply_to=True) 373 | return 374 | 375 | # 获取用户头像 376 | avatar_bytes = await PlatformUtils.get_user_avatar(str(uid), "qq", session.self_id) 377 | avatar = base64.b64encode(avatar_bytes).decode("utf-8") if avatar_bytes else "" 378 | 379 | # 构建模板数据 380 | result = { 381 | "avatar": f"data:image/png;base64,{avatar}", 382 | "name": await get_name(session), 383 | "records": [ 384 | { 385 | "action_icon": { 386 | "fencing": "🎮", 387 | "fenced": "🎯", 388 | "register": "📝", 389 | "gluing": "💦", 390 | "unsubscribe": "❌", 391 | "drug": "💊", 392 | }.get(record["action"], record["action"]), 393 | "action": { 394 | "fencing": "击剑", 395 | "fenced": "被击剑", 396 | "gluing": "打胶", 397 | "register": "注册牛牛", 398 | "unsubscribe": "注销牛牛", 399 | "drug": "使用药水", 400 | }.get(record["action"], record["action"]), 401 | "time": record["time"], 402 | "origin": record["origin_length"], 403 | "new": record["new_length"], 404 | "diff": f"+{record['diff']}" if record["diff"] > 0 else record["diff"], 405 | "diff_color": ( 406 | "positive" 407 | if record["diff"] > 0 408 | else "negative" 409 | if record["diff"] < 0 410 | else "neutral" 411 | ), 412 | } 413 | for record in records 414 | ], 415 | } 416 | 417 | # 渲染模板 418 | template_dir = Path(__file__).resolve().parent / "templates" 419 | pic = await template_to_pic( 420 | template_path=str(template_dir), 421 | template_name="record_info.html", 422 | templates=result, 423 | ) 424 | await niuniu_my_record.send(Image(raw=pic), reply_to=True) 425 | -------------------------------------------------------------------------------- /niuniu/meiboli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molanp/zhenxun_plugin_niuniu/b7d368d2952bc4a18f6c08cbc156aa307bf17489/niuniu/meiboli.png -------------------------------------------------------------------------------- /niuniu/menghanyao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molanp/zhenxun_plugin_niuniu/b7d368d2952bc4a18f6c08cbc156aa307bf17489/niuniu/menghanyao.png -------------------------------------------------------------------------------- /niuniu/model.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from tortoise import fields 4 | from tortoise.transactions import in_transaction 5 | 6 | from zhenxun.services.db_context import Model 7 | 8 | 9 | class NiuNiuUser(Model): 10 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 11 | """自增id""" 12 | uid = fields.CharField(255, description="用户唯一标识符") 13 | """用户id""" 14 | length = fields.FloatField(description="用户长度") 15 | """用户长度""" 16 | sex = fields.CharField(255, description="用户性别", default="boy") 17 | """用户性别""" 18 | time = fields.DatetimeField(auto_now_add=True) 19 | """创建时间""" 20 | 21 | class Meta: # pyright: ignore [reportIncompatibleVariableOverride] 22 | table = "niuniu_users" 23 | table_description = "牛牛大作战用户数据表" 24 | indexes: ClassVar = [("uid", "length")] 25 | 26 | async def save(self, *args, **kwargs) -> None: 27 | """保存时自动计算性别""" 28 | self.sex = "girl" if self.length <= 0 else "boy" 29 | await super().save(*args, **kwargs) 30 | 31 | @classmethod 32 | async def migrate_from_sqlite(cls): 33 | """从旧表迁移用户数据""" 34 | from .database import Sqlite 35 | 36 | async with in_transaction(): 37 | old_data = await Sqlite.query("users") 38 | for item in old_data: 39 | await cls.update_or_create( 40 | uid=str(item["uid"]), 41 | defaults={ 42 | "length": item["length"], 43 | "sex": item["sex"], 44 | "time": item["time"], 45 | }, 46 | ) 47 | 48 | 49 | class NiuNiuRecord(Model): 50 | id = fields.IntField(pk=True, generated=True, auto_increment=True) 51 | """自增id""" 52 | uid = fields.TextField(description="用户唯一标识符") 53 | """用户id""" 54 | action = fields.TextField(description="动作名称") 55 | """动作名称""" 56 | origin_length = fields.FloatField(description="操作前长度") 57 | """操作前长度""" 58 | diff = fields.FloatField(description="长度变化") 59 | """长度变化""" 60 | new_length = fields.FloatField(description="操作后长度") 61 | """操作后长度""" 62 | time = fields.DatetimeField(auto_now_add=True) 63 | """创建时间""" 64 | 65 | class Meta: # pyright: ignore [reportIncompatibleVariableOverride] 66 | table = "niuniu_record" 67 | table_description = "牛牛大作战日志表" 68 | indexes: ClassVar = [("uid", "action")] 69 | 70 | async def save(self, *args, **kwargs) -> None: 71 | """保存时自动计算长度差值""" 72 | if self.new_length is not None and self.origin_length is not None: 73 | self.diff = round(self.new_length - self.origin_length, 2) 74 | await super().save(*args, **kwargs) 75 | 76 | @classmethod 77 | async def migrate_from_sqlite(cls): 78 | """从旧表迁移记录数据""" 79 | from .database import Sqlite 80 | 81 | async with in_transaction(): 82 | old_data = await Sqlite.query("records") 83 | for item in old_data: 84 | await cls.create( 85 | uid=str(item["uid"]), 86 | action=item["action"], 87 | origin_length=item["origin_length"], 88 | new_length=item["new_length"], 89 | time=item["time"], 90 | ) 91 | -------------------------------------------------------------------------------- /niuniu/niuniu.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from nonebot import get_bot, require 4 | require("nonebot_plugin_uninfo") 5 | from nonebot_plugin_uninfo import Uninfo 6 | from tortoise.functions import Max, Min 7 | 8 | from zhenxun.services.log import logger 9 | from zhenxun.utils.image_utils import BuildImage, ImageTemplate 10 | from zhenxun.utils.platform import PlatformUtils 11 | 12 | from .model import NiuNiuRecord, NiuNiuUser 13 | 14 | 15 | class NiuNiu: 16 | @classmethod 17 | async def get_length(cls, uid: int | str) -> float | None: 18 | user = await NiuNiuUser.get_or_none(uid=uid).values("length") 19 | return user["length"] if user else None 20 | 21 | @classmethod 22 | async def record_length( 23 | cls, uid: int | str, origin_length: float, new_length: float, action: str 24 | ): 25 | await NiuNiuRecord.create( 26 | uid=uid, 27 | origin_length=round(origin_length, 2), 28 | new_length=round(new_length, 2), 29 | action=action, 30 | ) 31 | 32 | @classmethod 33 | async def update_length(cls, uid: int | str, new_length: float): 34 | await NiuNiuUser.filter(uid=uid).update(length=new_length) 35 | 36 | @classmethod 37 | async def apply_decay(cls, current_length: float) -> float: 38 | """动态衰减核心算法(优化负值处理)""" 39 | decay_rate = 0.02 # 基础衰减率 40 | 41 | # 动态调整规则(新增负值衰减限制) 42 | if current_length > 50: 43 | decay_rate += min(0.1, (current_length - 50) * 0.005) 44 | elif current_length < -50: 45 | # 负值超过-50后,衰减率增幅减半 46 | decay_rate -= max(-0.05, (current_length + 50) * 0.0025) # 调整系数 47 | 48 | if current_length > 0: 49 | return max(0, current_length * (1 - decay_rate)) 50 | # 添加衰减幅度限制和最小值限制 51 | decayed = current_length * (1 + decay_rate) 52 | min_length = -100 # 设置物理下限 53 | return max(min_length, decayed * 0.8) # 负值衰减幅度减少20% 54 | 55 | @classmethod 56 | async def random_length(cls) -> float: 57 | users = await NiuNiuUser.all().values("length") 58 | 59 | if not users: 60 | origin_length = 10 61 | else: 62 | length_values = [u["length"] for u in users] 63 | index = min(int(len(length_values) * 0.3), len(length_values) - 1) 64 | origin_length = float(length_values[index]) 65 | return round(origin_length * 0.9, 2) 66 | 67 | @classmethod 68 | async def latest_gluing_time(cls, uid: int) -> str: 69 | record = ( 70 | await NiuNiuRecord.filter(uid=uid, action="gluing") 71 | .order_by("-time") 72 | .first() 73 | .values("time") 74 | ) 75 | 76 | return record["time"] if record else "暂无记录" 77 | 78 | @classmethod 79 | async def get_nearest_lengths(cls, target_length: float) -> list[float]: 80 | # 使用ORM聚合查询 81 | greater_length = ( 82 | await NiuNiuUser.filter(length__gt=target_length) 83 | .annotate(min_length=Min("length")) 84 | .values("min_length") 85 | ) 86 | 87 | less_length = ( 88 | await NiuNiuUser.filter(length__lt=target_length) 89 | .annotate(max_length=Max("length")) 90 | .values("max_length") 91 | ) 92 | 93 | return [ 94 | ( 95 | greater_length[0]["min_length"] 96 | if greater_length and greater_length[0]["min_length"] is not None 97 | else 0 98 | ), 99 | ( 100 | less_length[0]["max_length"] 101 | if less_length and less_length[0]["max_length"] is not None 102 | else 0 103 | ), 104 | ] 105 | 106 | @classmethod 107 | async def last_fenced_time(cls, uid: int | str) -> float: 108 | record = ( 109 | await NiuNiuRecord.filter(uid=uid, action="fenced") 110 | .order_by("-time") 111 | .first() 112 | ) 113 | 114 | return record.time.timestamp() if record else 0 115 | 116 | @classmethod 117 | async def gluing( 118 | cls, origin_length: float, discount: float = 1, reduce: bool = False 119 | ) -> tuple[float, float]: 120 | result = await cls.get_nearest_lengths(origin_length) 121 | if result[0] != 0 and result[1] != 0: 122 | growth_factor = max(0.5, 1 - abs(origin_length) / 200) # 长度越大增长越慢 123 | new_length = ( 124 | origin_length 125 | + (result[0] * 0.3 - result[1] * 0.3) * growth_factor * discount 126 | ) 127 | return round(new_length, 2), round(new_length - origin_length, 2) 128 | 129 | prob = random.choice([-0.6, -0.5, -0.4, -0.2, 0, 0.2, 0.4, 0.5, 0.6]) 130 | if origin_length <= 0: 131 | diff = prob * 0.1 * origin_length * -1 132 | else: 133 | diff = prob * 0.1 * origin_length 134 | if reduce: 135 | diff = diff * -1 136 | raw_new_length = origin_length + diff * discount 137 | new_length = await cls.apply_decay(raw_new_length) 138 | return round(new_length, 2), round(new_length - origin_length, 2) 139 | 140 | @classmethod 141 | async def comment(cls, length: float) -> str: 142 | if length <= -50: 143 | return ( 144 | "哇哦!你已经进化成魅魔了!" 145 | "魅魔在击剑时有20%的几率消耗自身长度吞噬对方牛牛呢!" 146 | ) 147 | elif -50 < length <= -25: 148 | return random.choice( 149 | [ 150 | "嗯……好像已经穿过了身体吧……从另一面来看也可以算是凸出来的吧?", 151 | "这名女生,你的身体很健康哦!", 152 | "WOW,真的凹进去了好多呢!", 153 | "你已经是我们女孩子的一员啦!", 154 | ] 155 | ) 156 | elif -25 < length <= -10: 157 | return random.choice( 158 | [ 159 | "你已经是一名女生了呢!", 160 | "从女生的角度来说,你发育良好哦!", 161 | "你醒啦?你已经是一名女孩子啦!", 162 | "唔……可以放进去一根手指了都……", 163 | ] 164 | ) 165 | elif -10 < length <= 0: 166 | return random.choice( 167 | [ 168 | "安了安了,不要伤心嘛,做女生有什么不好的啊.", 169 | "不哭不哭,摸摸头,虽然很难再长出来,但是请不要伤心啦啊!", 170 | "加油加油!我看好你哦!", 171 | "你醒啦?你现在已经是一名女孩子啦!", 172 | "成为香香软软的女孩子吧!", 173 | ] 174 | ) 175 | elif 0 < length <= 10: 176 | return random.choice( 177 | [ 178 | "你行不行啊?细狗!", 179 | "虽然短,但是小小的也很可爱呢.", 180 | "像一只蚕宝宝.", 181 | "长大了.", 182 | ] 183 | ) 184 | elif 10 < length <= 25: 185 | return random.choice( 186 | [ 187 | "唔……没话说", 188 | "已经很长了呢!", 189 | ] 190 | ) 191 | elif 25 < length <= 50: 192 | return random.choice( 193 | [ 194 | "话说这种真的有可能吗?", 195 | "厚礼谢!", 196 | "已经突破天际了嘛……", 197 | "唔……这玩意应该不会变得比我高吧?", 198 | "你这个长度会死人的……!", 199 | "你马上要进化成牛头人了!!", 200 | "你是什么怪物,不要过来啊!!", 201 | ] 202 | ) 203 | else: 204 | return ( 205 | "惊世骇俗!你已经进化成牛头人了!" 206 | "牛头人在击剑时有20%的几率消耗自身长度吞噬对方牛牛呢!" 207 | ) 208 | 209 | @classmethod 210 | async def rank( 211 | cls, num: int, session: Uninfo, deep: bool = False, is_all: bool = False 212 | ) -> BuildImage | str: 213 | data_list = [] 214 | order = "length" if deep else "-length" 215 | 216 | filter_condition = {"length__lte": 0} if deep else {"length__gt": 0} 217 | query = NiuNiuUser.filter(**filter_condition).order_by(order) 218 | uid2name = {} 219 | bot = get_bot(self_id=session.self_id) 220 | 221 | if not is_all and session.group: 222 | try: 223 | group_members = await bot.get_group_member_list( 224 | group_id=session.group.id 225 | ) 226 | user_ids = [str(member["user_id"]) for member in group_members] 227 | query = query.filter(uid__in=user_ids) 228 | uid2name = { 229 | str(member["user_id"]): member["nickname"] 230 | for member in group_members 231 | } 232 | except Exception as e: 233 | logger.error("获取群成员失败", "niuniu", e=e) 234 | return f"获取群成员失败: {e!s}" 235 | 236 | # 执行查询并转换数据 237 | users = await query.limit(num).values("uid", "length") 238 | if not users: 239 | return "暂无此数据..." 240 | 241 | user_id_list = [user["uid"] for user in users] 242 | index = ( 243 | user_id_list.index(session.user.id) + 1 244 | if session.user.id in user_id_list 245 | else "-1(未统计)" 246 | ) 247 | 248 | for i, user in enumerate(users): 249 | uid = user["uid"] 250 | length = user["length"] 251 | 252 | avatar_bytes = await PlatformUtils.get_user_avatar( 253 | uid, "qq", session.self_id 254 | ) 255 | 256 | nickname = ( 257 | uid2name.get(uid) 258 | or (await bot.get_stranger_info(user_id=uid))["nickname"] 259 | ) 260 | 261 | data_list.append([f"{i + 1}", (avatar_bytes, 30, 30), nickname, length]) 262 | 263 | # 生成标题 264 | title_type = "深度" if deep else "长度" 265 | scope = "全局" if is_all else "群组内" 266 | title = f"{title_type}{scope}排行" 267 | tip = f"你的排名在{scope}第 {index} 位哦!" 268 | 269 | return await ImageTemplate.table_page( 270 | head_text=title, 271 | tip_text=tip, 272 | column_name=["排名", "头像", "名称", "长度"], 273 | data_list=data_list, 274 | ) 275 | 276 | @classmethod 277 | async def get_user_records(cls, uid: int | str, num: int = 10) -> list[dict]: 278 | """ 279 | 获取指定用户的战绩记录 280 | 281 | :param uid: 用户ID 282 | :param num: 记录数量 283 | :return: 记录列表 284 | """ 285 | return await NiuNiuRecord.filter(uid=uid).order_by("-time").limit(num).values() 286 | -------------------------------------------------------------------------------- /niuniu/niuniu_goods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molanp/zhenxun_plugin_niuniu/b7d368d2952bc4a18f6c08cbc156aa307bf17489/niuniu/niuniu_goods/__init__.py -------------------------------------------------------------------------------- /niuniu/niuniu_goods/event_manager.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import random 3 | import time 4 | 5 | from .model import GlueEvent, PropModel, load_events 6 | from ..niuniu import NiuNiu 7 | from ..utils import UserState 8 | from .goods import get_prop_by_name, get_event_buff 9 | 10 | 11 | def choose_description( 12 | diff: float, 13 | positive_descs: list[str] | None, 14 | negative_descs: list[str] | None, 15 | no_change_descs: list[str] | None, 16 | ) -> tuple[str, bool]: 17 | """选择随机描述""" 18 | if diff > 0 and positive_descs: 19 | return random.choice(positive_descs), False 20 | elif diff < 0 and negative_descs: 21 | return random.choice(negative_descs), True 22 | elif diff == 0 and no_change_descs: 23 | return random.choice(no_change_descs), False 24 | return random.choice(positive_descs or negative_descs or ["无描述"]), False 25 | 26 | 27 | async def process_glue_event( 28 | uid: str, 29 | origin_length: float, 30 | is_rapid: bool, 31 | ) -> tuple[str, float, float]: 32 | """处理打胶事件""" 33 | 34 | events = await adjust_glue_effects(uid) 35 | 36 | # 根据权重选择事件 37 | event_names = list(events.keys()) 38 | weights = [e.weight for e in events.values()] 39 | selected = random.choices(event_names, weights=weights, k=1)[0] 40 | event = events[selected] 41 | 42 | if event.buff: 43 | await apply_buff(uid, event.buff) 44 | 45 | # 处理连续打胶事件 46 | if is_rapid and event.rapid_effect: 47 | rapid_effect = event.rapid_effect 48 | if rapid_effect.coefficient: 49 | new_length, diff = await NiuNiu.gluing( 50 | origin_length, rapid_effect.coefficient 51 | ) 52 | elif rapid_effect.effect: 53 | new_length = round(abs(origin_length) * rapid_effect.effect, 2) 54 | diff = new_length - origin_length 55 | else: 56 | new_length = origin_length 57 | diff = 0 58 | 59 | desc_template, need_abs = choose_description( 60 | diff, 61 | rapid_effect.positive_descriptions, 62 | rapid_effect.negative_descriptions, 63 | rapid_effect.no_change_descriptions, 64 | ) 65 | result = desc_template.format( 66 | diff=round(abs(diff) if need_abs else diff, 2), 67 | new_length=round(new_length, 2), 68 | ban_time=rapid_effect.ban_time, 69 | ) 70 | else: 71 | # 处理普通事件逻辑 72 | if event.category in ["growth", "special"]: 73 | new_length, diff = await NiuNiu.gluing(origin_length, event.coefficient) 74 | elif event.category == "reduce": 75 | new_length, diff = await NiuNiu.gluing(origin_length, reduce=True) 76 | elif event.category == "shrinkage": 77 | if origin_length >= 0: 78 | new_length = round(origin_length * event.effect, 2) 79 | else: 80 | new_length = round(origin_length / event.effect, 2) 81 | diff = new_length - origin_length 82 | elif event.category == "arrested": 83 | new_length = origin_length 84 | diff = 0 85 | else: 86 | raise ValueError(f"Invalid event category: {event.category}") 87 | 88 | # 处理连续子事件 89 | # if event.next_event and event.next_event in events: 90 | # result, new_length, diff = await process_glue_event( 91 | # uid, new_length, is_rapid 92 | # ) 93 | # return result, new_length, diff 94 | 95 | desc_template, need_abs = choose_description( 96 | diff, 97 | event.positive_descriptions, 98 | event.negative_descriptions, 99 | event.no_change_descriptions, 100 | ) 101 | result = desc_template.format( 102 | diff=round(abs(diff) if need_abs else diff, 2), 103 | new_length=round(new_length, 2), 104 | ban_time=event.ban_time, 105 | ) 106 | 107 | return result, new_length, diff 108 | 109 | 110 | async def use_prop(uid: str, prop_name: str) -> str: 111 | """使用道具来调整击剑胜率和打胶效果""" 112 | prop = get_prop_by_name(prop_name) 113 | if prop is None: 114 | return "无效的道具" 115 | 116 | # 计算过期时间 117 | expire_time = time.time() + prop.duration 118 | prop.expire_time = expire_time 119 | 120 | # 更新道具状态 121 | await UserState.update("buff_map", uid, prop) 122 | 123 | return f"使用了 {prop.name},效果持续至 {time.ctime(prop.expire_time)}" 124 | 125 | 126 | async def apply_buff(uid: str, name: str) -> bool: 127 | """使用事件buff来调整击剑胜率和打胶效果""" 128 | buff = get_event_buff(name) 129 | if buff is None: 130 | return "无效的事件" 131 | 132 | # 计算过期时间 133 | expire_time = time.time() + buff.duration 134 | buff.expire_time = expire_time 135 | 136 | # 更新道具状态 137 | await UserState.update("buff_map", uid, buff) 138 | return True 139 | 140 | 141 | async def adjust_glue_effects(uid: str) -> dict[str, GlueEvent]: 142 | events = await load_events() 143 | buff = await get_buffs(uid) 144 | glue_effect = buff.glue_effect 145 | 146 | for event in events.values(): 147 | if event.affected_by_props: 148 | if event.coefficient: 149 | event.coefficient *= glue_effect 150 | if event.effect: 151 | event.effect = event.effect * glue_effect 152 | if event.category in ["shrinkage", "arrested"]: 153 | event.weight *= buff.glue_negative_weight 154 | return events 155 | 156 | 157 | async def get_buffs(uid: str) -> PropModel: 158 | """ 159 | 获取用户的 buff,如果已过期,则清除并返回空。 160 | 161 | :param uid: 用户的唯一标识 162 | :return: 用户的 buff 信息(未过期)或 None 163 | """ 164 | # 获取 buff 信息 165 | buff_info = await UserState.get("buff_map", uid) 166 | 167 | # 如果 buff 存在且未过期 168 | if buff_info and buff_info.expire_time > time.time(): 169 | return buff_info 170 | 171 | # 清除过期道具 172 | with contextlib.suppress(KeyError): 173 | await UserState.del_key("buff_map", uid) 174 | # 返回空,表示该用户的 buff 已过期 175 | return PropModel(name="None",) 176 | -------------------------------------------------------------------------------- /niuniu/niuniu_goods/events.yaml: -------------------------------------------------------------------------------- 1 | normal: 2 | weight: 60 3 | positive_descriptions: 4 | - 你嘿咻嘿咻一下,促进了牛牛发育,牛牛增加了{diff}cm了呢!🎉 5 | - 你打了个舒服痛快的🦶呐,牛牛增加了{diff}cm呢!💪 6 | - 哇哦!你的一🦶让牛牛变长了{diff}cm!👏 7 | - 你的牛牛感受到了你的热情,增加了{diff}cm!🔥 8 | - 你的一脚仿佛有魔力,牛牛增加了{diff}cm!✨ 9 | no_change_descriptions: 10 | - 你打了个🦶,但是什么变化也没有,好奇怪捏~🤷♂️ 11 | - 你的牛牛刚开始变长了,可过了一会又回来了,什么变化也没有,好奇怪捏~🤷♀️ 12 | - 你准备🦌的时候发现今天是疯狂星期四,先V我50!😄 13 | - 你的牛牛看起来很开心,但没有变化!😊 14 | - 你在打胶时,感觉这个世界似乎发生了什么变化🤔 15 | negative_descriptions: 16 | - 哦吼!?看来你的牛牛凹进去了{diff}cm呢!😱 17 | - 你突发恶疾!你的牛牛凹进去了{diff}cm!😨 18 | - 笑死,你因为打🦶过度导致牛牛凹进去了{diff}cm!🤣🤣🤣 19 | - 你的牛牛仿佛被你一🦶踢进了地缝,凹进去了{diff}cm!🕳️ 20 | - 你的一🦶用力过度了,牛牛凹进去了{diff}cm!💥 21 | - 阿哦,你过度打🦶,牛牛缩短了{diff}cm了呢!😢 22 | - 🦌的时候突然响起了届かない恋!你听了之后想起了自己的往事, 伤心之余发觉牛牛缩短了{diff}cm。 23 | - 你的牛牛变长了很多,你很激动地继续打🦶,然后牛牛缩短了{diff}cm呢!🤦♂️ 24 | - 小打怡情,大打伤身,强打灰飞烟灭!你过度打🦶,牛牛缩短了{diff}cm捏!💥 25 | - 你的牛牛看起来很受伤,缩短了{diff}cm!🤕 26 | - 你的打🦶没效果,于是很气急败坏地继续打🦶,然后牛牛缩短了{diff}cm呢!🤦♂️ 27 | - 🦌太多次导致身体虚弱,牛牛长度减少了{diff}cm! 28 | coefficient: 1 29 | category: growth 30 | buff: "冷静期" 31 | rapid_effect: 32 | positive_descriptions: 33 | - 这么着急?牛牛只微微增长了{diff}cm...🤏 34 | - bro你搞这么快只会适得其反!牛牛只增加{diff}cm!😰 35 | negative_descriptions: 36 | - 这么着急?牛牛却减少了{diff}cm...😢 37 | - bro你搞这么快只会适得其反!牛牛却减少了{diff}cm!😭 38 | effect: 0.9 39 | buff: "樯橹灰飞烟灭" 40 | shrinkage: 41 | weight: 10 42 | negative_descriptions: 43 | - 由于你在换蛋期打胶,你的牛牛断掉了呢!当前长度{new_length}cm!🤯 44 | - bro换蛋期就不要打胶了!你的牛牛萎缩了{diff}cm!💩 45 | effect: 0.5 46 | category: shrinkage 47 | arrested: 48 | weight: 10 49 | positive_descriptions: 50 | - 打胶时被窗外的路人发现了,对方报警了,你被抓走了!关进小黑屋里{ban_time}s! 51 | ban_time: 180 52 | category: arrested 53 | special_boost: 54 | weight: 5 55 | positive_descriptions: 56 | - 你收到了群主私发的女装,冲!!!牛牛长大了{diff}cm!👗 57 | coefficient: 1.1 58 | category: special 59 | buff: "精神亢奋" 60 | rapid_penalty: 61 | weight: 15 62 | positive_descriptions: 63 | - 这么着急?牛牛只微微增长了{diff}cm...🤏 64 | - bro你搞这么快只会适得其反!牛牛只增加{diff}cm!😰 65 | negative_descriptions: 66 | - 这么着急?牛牛减少了{diff}cm...😢 67 | - bro你搞这么快只会适得其反!牛牛减少了{diff}cm!😭 68 | category: reduce 69 | -------------------------------------------------------------------------------- /niuniu/niuniu_goods/goods.py: -------------------------------------------------------------------------------- 1 | from .model import PropModel 2 | 3 | GOODS = [ 4 | PropModel( 5 | name="伟哥", 6 | price=200, 7 | des="神秘小药丸,下次抽到击剑胜率的概率翻倍,持续10分钟", 8 | icon="weige.png", 9 | duration=60*10, 10 | fencing_weight=1.5, 11 | ), 12 | PropModel( 13 | name="蒙汗药", 14 | price=250, 15 | icon="menghanyao.png", 16 | des="给对方打胶CD加长300s", 17 | ), 18 | PropModel( 19 | name="鱼板", 20 | price=300, 21 | icon="yuban.png", 22 | duration=60*20, 23 | des="会让人变得香香软软的东西,使用后自己下次打胶变短概率翻倍,遇到负面效果概率翻倍,持续20分钟", 24 | glue_effect=0.3, 25 | glue_negative_weight=5, 26 | ), 27 | PropModel( 28 | name="美波里的神奇药水", 29 | price=500, 30 | des="谁知道会有什么效果呢", 31 | icon="meiboli.png", 32 | ) 33 | ] 34 | 35 | EVENT_BUFF_MAP = [ 36 | PropModel( 37 | name="冷静期", 38 | duration=300, 39 | glue_effect=0.9, 40 | glue_negative_weight=1.1 41 | ), 42 | PropModel( 43 | name="精神亢奋", 44 | duration=400, 45 | glue_effect=1.1 46 | ), 47 | PropModel( 48 | name="樯橹灰飞烟灭", 49 | duration=500, 50 | glue_effect=0.3, 51 | glue_negative_weight=3 52 | ) 53 | ] 54 | 55 | 56 | def get_event_buff(name: str) -> PropModel: 57 | """ 58 | 通过名称获取指定的Buff。 59 | 60 | Args: 61 | name (str): Buff的名称 62 | 63 | Returns: 64 | PropModel: 如果找到匹配的Buff,返回该实例 65 | """ 66 | buff = next((buff for buff in EVENT_BUFF_MAP if buff.name == name), None) 67 | if buff is None: 68 | raise ValueError("不存在该事件") 69 | return buff 70 | 71 | 72 | def is_prop_in_list(prop_name: str) -> bool: 73 | """ 74 | 判断一个道具是否在道具列表中。 75 | 76 | Args: 77 | prop_name (str): 道具的名称 78 | 79 | Returns: 80 | bool: 如果道具在列表中,返回 True;否则返回 False 81 | """ 82 | return any(prop.name == prop_name for prop in GOODS) 83 | 84 | 85 | def get_prop_by_name(prop_name: str) -> PropModel | None: 86 | """ 87 | 通过名称获取指定的道具。 88 | 89 | Args: 90 | prop_name (str): 道具的名称 91 | 92 | Returns: 93 | PropModel | None: 如果找到匹配的道具,返回该道具实例;否则返回 None 94 | """ 95 | return next((prop for prop in GOODS if prop.name == prop_name), None) 96 | -------------------------------------------------------------------------------- /niuniu/niuniu_goods/model.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import aiofiles 4 | from pydantic import BaseModel 5 | import yaml 6 | 7 | 8 | class RapidEffect(BaseModel): 9 | """ 10 | 连续打胶效果模型,用于存储连续打胶事件的描述文本、系数和禁用时间。 11 | 12 | Attributes: 13 | positive_descriptions (list[str] | None): 事件增加文本 14 | no_change_descriptions (list[str] | None): 事件不变文本 15 | negative_descriptions (list[str] | None): 事件减少文本 16 | coefficient (float): 系数 17 | effect (float): 效果系数 18 | ban_time (int): 禁用时间(秒) 19 | """ 20 | 21 | positive_descriptions: list[str] | None = None 22 | """事件增加文本""" 23 | no_change_descriptions: list[str] | None = None 24 | """事件不变文本""" 25 | negative_descriptions: list[str] | None = None 26 | """事件减少文本""" 27 | coefficient: float = 1 28 | """增加比例系数""" 29 | effect: float = 1 30 | """效果系数""" 31 | ban_time: int = 0 32 | """小黑屋时间""" 33 | 34 | 35 | class GlueEvent(BaseModel): 36 | """ 37 | 打胶事件模型,用于存储打胶事件的详细配置。 38 | 39 | Attributes: 40 | weight (int): 事件权重 41 | positive_descriptions (list[str] | None): 事件增加文本 42 | no_change_descriptions (list[str] | None): 事件不变文本 43 | negative_descriptions (list[str] | None): 事件减少文本 44 | coefficient (float): 系数 45 | effect (float): 效果系数 46 | ban_time (int): 禁用时间(秒) 47 | category (str): 事件类型 48 | buff (Buff | None): Buff 效果 49 | next_event (str | None): 连续子事件 50 | rapid_effect (RapidEffect | None): 连续打胶效果 51 | affected_by_props (bool): 是否受到道具影响 52 | """ 53 | 54 | weight: float 55 | """事件权重""" 56 | positive_descriptions: list[str] | None = None 57 | """事件增加文本""" 58 | no_change_descriptions: list[str] | None = None 59 | """事件不变文本""" 60 | negative_descriptions: list[str] | None = None 61 | """事件减少文本""" 62 | coefficient: float = 1 63 | """增加比例系数""" 64 | effect: float = 1 65 | """效果系数""" 66 | ban_time: int = 0 67 | """小黑屋时间""" 68 | category: str 69 | """事件类型""" 70 | buff: str | None = None 71 | """Buff""" 72 | next_event: str | None = None 73 | """连续子事件""" 74 | rapid_effect: RapidEffect | None = None 75 | """连续打胶效果""" 76 | affected_by_props: bool = True 77 | """是否受到道具影响""" 78 | 79 | 80 | class PropModel(BaseModel): 81 | """ 82 | 道具模型,用于存储道具的持续时间、击剑胜率倍数、打胶效果倍数和打胶触发负面事件的概率倍数。 83 | 84 | Attributes: 85 | name (str): 道具的名称 86 | des (str): 道具的简介 87 | price (int): 道具的价格 88 | icon (str): 道具的图标 89 | duration (int): Buff 持续时间(秒) 90 | fencing_weight (float): 击剑胜率的倍数 91 | glue_effect (float): 打胶效果的倍数 92 | glue_negative_weight (float): 打胶触发负面事件的倍数 93 | """ 94 | name: str 95 | """道具的名称""" 96 | des: str = "" 97 | """道具的简介""" 98 | price: int = -1 99 | """道具的价格""" 100 | icon: str = "" 101 | """道具的图标""" 102 | duration: int = 0 103 | """Buff 持续时间(秒)""" 104 | fencing_weight: float = 1 105 | """击剑胜率的倍数""" 106 | glue_effect: float = 1 107 | """打胶效果的倍数""" 108 | glue_negative_weight: float = 1 109 | """打胶触发负面事件的倍数""" 110 | expire_time: float = 0 111 | """过期时间""" 112 | 113 | 114 | async def load_events() -> dict[str, GlueEvent]: 115 | """ 116 | 加载事件配置文件,返回一个包含打胶事件的字典。 117 | 118 | Returns: 119 | dict[str, GlueEvent]: 包含打胶事件的字典,键为事件名称,值为 GlueEvent 实例 120 | """ 121 | config_path = Path(__file__).parent / "events.yaml" 122 | async with aiofiles.open(config_path, encoding="utf-8") as f: 123 | content = await f.read() 124 | config = yaml.safe_load(content) 125 | 126 | 127 | return {key: GlueEvent(**value) for key, value in config.items()} 128 | -------------------------------------------------------------------------------- /niuniu/shop.py: -------------------------------------------------------------------------------- 1 | import time 2 | from nonebot import require 3 | require("nonebot_plugin_uninfo") 4 | 5 | from nonebot_plugin_uninfo import Uninfo 6 | 7 | from zhenxun.services.log import logger 8 | from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_register 9 | 10 | from .niuniu import NiuNiu 11 | from .niuniu_goods.event_manager import use_prop 12 | from .niuniu_goods.goods import GOODS 13 | from .utils import UserState 14 | 15 | 16 | def create_handler(good): 17 | if good.name == "蒙汗药": 18 | 19 | async def handler(user_id: str, at_users: list[str], session: Uninfo): # type: ignore 20 | if len(at_users) > 1: 21 | raise NotMeetUseConditionsException("你的蒙汗药只能对一位玩家使用哦!") 22 | if not at_users: 23 | raise NotMeetUseConditionsException("@人了吗?你要对你自己使用吗?") 24 | if at_users[0] == user_id: 25 | raise NotMeetUseConditionsException("不能对自己使用哦!请@一位玩家") 26 | uid = at_users[0] 27 | logger.info(f"{uid} 被 {user_id} 使用了蒙汗药", session=session) 28 | await UserState.update( 29 | "gluing_time_map", 30 | uid, 31 | (await UserState.get("gluing_time_map", uid, time.time())) + 300, 32 | ) 33 | 34 | elif good.name == "美波里的神奇药水": 35 | 36 | async def handler(user_id: str, session: Uninfo): # type: ignore 37 | origin_length = await NiuNiu.get_length(user_id) 38 | if origin_length is None: 39 | raise NotMeetUseConditionsException("你没有牛牛数据哦!") 40 | new_length = origin_length * -1 41 | await NiuNiu.update_length(user_id, new_length) 42 | await NiuNiu.record_length(user_id, origin_length, new_length, "drug") 43 | logger.info(f"{user_id} 使用了美波里的神奇药水", session=session) 44 | return "你使用了美波里的神奇药水,性别发生了逆转" 45 | 46 | else: 47 | 48 | async def handler(user_id: str, session: Uninfo): # type: ignore 49 | result = await use_prop(user_id, good.name) 50 | logger.info(f"{result}", session=session) 51 | 52 | return handler 53 | 54 | 55 | for good in GOODS: 56 | shop_register( 57 | name=good.name, 58 | price=good.price, 59 | des=good.des, 60 | load_status=True, 61 | icon=good.icon, 62 | partition="牛牛商店", 63 | )(create_handler(good)) 64 | -------------------------------------------------------------------------------- /niuniu/templates/my_info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |{{ buff.name }}
77 |击剑胜率倍率: x{{ buff.fencing_weight }}
78 |打胶倍率: x{{ buff.glue_effect }}
79 |负面事件倍率: x{{ buff.glue_negative_weight }}
80 |⏳ 剩余时间: {{ (buff.expire_time - now) | round(1) }}s
81 |