""" 群聊文字修仙插件 说明: - 原子指令 + 状态机 + 限流 + Cache-Aside - 读优先 Redis;写 DB 后删除缓存,由读取路径回填 - 群频次限制与用户冷却共同防骚扰/防封 """ import json import time import random from datetime import datetime, timedelta from typing import Dict, Any, List, Optional, Tuple from loguru import logger from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.decorator.points_decorator import plugin_points_cost from utils.decorator.rate_limit_decorator import user_feature_rate_limit from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager from wechat_ipad import WechatAPIClient from db.connection import DBConnectionManager from db.xiuxian_db import XiuxianDB from db.points_db import PointsDBOperator, PointSource from datetime import timezone class XiuxianRedisDB: """修仙插件的 Redis 访问封装:玩家缓存、限流键与排行榜。""" def __init__(self, db_manager: DBConnectionManager): self.db_manager = db_manager self.player_prefix = "xiuxian:cache:player:" self.leaderboard_key = None self.leaderboard_realm_key = None def set_leaderboard_key(self, key: str): self.leaderboard_key = key def set_realm_leaderboard_key(self, key: str): self.leaderboard_realm_key = key def get_redis(self): return self.db_manager.get_redis_connection() def get_player(self, user_id: str, group_id: str) -> Optional[Dict[str, Any]]: """读取玩家数据,兼容旧格式(无group_id)和新格式(有group_id)""" try: with self.get_redis() as r: # 优先尝试新格式:xiuxian:cache:player:{group_id}:{user_id} new_key = f"{self.player_prefix}{group_id}:{user_id}" data = r.get(new_key) if data: if isinstance(data, bytes): data = data.decode("utf-8") return json.loads(data) # 兼容旧格式:xiuxian:cache:player:{user_id} old_key = f"{self.player_prefix}{user_id}" data = r.get(old_key) if data: if isinstance(data, bytes): data = data.decode("utf-8") player = json.loads(data) # 如果旧数据没有 group_id,使用传入的 group_id if not player.get("group_id"): player["group_id"] = group_id # 迁移到新格式并删除旧格式 r.set(new_key, json.dumps(player, ensure_ascii=False)) r.delete(old_key) return player return None except Exception as e: logger.error(f"读取玩家数据失败: {e}") return None def save_player(self, player: Dict[str, Any]) -> bool: """保存玩家数据到新格式,同时删除旧格式(如果存在)""" try: with self.get_redis() as r: user_id = player.get("user_id") group_id = player.get("group_id", "") new_key = f"{self.player_prefix}{group_id}:{user_id}" old_key = f"{self.player_prefix}{user_id}" # 保存到新格式 r.set(new_key, json.dumps(player, ensure_ascii=False)) # 删除旧格式(如果存在),确保数据一致性 r.delete(old_key) return True except Exception as e: logger.error(f"保存玩家数据失败: {e}") return False def invalidate_player(self, user_id: str, group_id: str): """失效玩家缓存,同时删除新旧两种格式""" try: with self.get_redis() as r: new_key = f"{self.player_prefix}{group_id}:{user_id}" old_key = f"{self.player_prefix}{user_id}" r.delete(new_key) r.delete(old_key) # 兼容删除旧格式 except Exception as e: logger.error(f"失效玩家缓存失败: {e}") def set_rate_limit(self, user_id: str, group_id: str, cmd: str, seconds: int) -> bool: """设置限流,支持自然日重置(当seconds >= 86400时)""" try: with self.get_redis() as r: # 如果是24小时或更长的限流(如签到),使用基于日期的键 if seconds >= 86400: # 使用自然日:键包含日期,过期时间设为到明天0点的秒数 now = datetime.now(timezone.utc) # 获取明天的0点(UTC) tomorrow = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) # 计算到明天0点的秒数 expire_seconds = int((tomorrow - now).total_seconds()) # 使用日期作为键的一部分,包含group_id date_str = now.strftime("%Y-%m-%d") key = f"xiuxian:rate_limit:group:{group_id}:user:{user_id}:cmd:{cmd}:date:{date_str}" r.setex(key, expire_seconds, "1") else: # 短时间限流,使用原来的逻辑,包含group_id key = f"xiuxian:rate_limit:group:{group_id}:user:{user_id}:cmd:{cmd}" r.setex(key, seconds, "1") return True except Exception as e: logger.error(f"设置限流失败: {e}") return False def check_rate_limited(self, user_id: str, group_id: str, cmd: str) -> bool: """检查限流,支持自然日重置(当限流时间 >= 86400秒时)""" try: with self.get_redis() as r: # 检查是否是长时间限流(如签到) # 先尝试基于日期的键(新格式,包含group_id) now = datetime.now(timezone.utc) date_str = now.strftime("%Y-%m-%d") date_key = f"xiuxian:rate_limit:group:{group_id}:user:{user_id}:cmd:{cmd}:date:{date_str}" key_no_date = f"xiuxian:rate_limit:group:{group_id}:user:{user_id}:cmd:{cmd}" if r.exists(date_key) == 1 or r.exists(key_no_date) == 1: return True return False except Exception as e: logger.error(f"检查限流失败: {e}") return False def leaderboard_add(self, group_id: str, user_id: str, score: float): try: if not self.leaderboard_key: return with self.get_redis() as r: r.zadd(f"{self.leaderboard_key}:{group_id}", {user_id: score}) except Exception as e: logger.error(f"更新排行榜失败: {e}") def leaderboard_top(self, group_id: str, top_n: int = 10) -> List[Tuple[str, float]]: try: if not self.leaderboard_key: return [] with self.get_redis() as r: res = r.zrevrange(f"{self.leaderboard_key}:{group_id}", 0, top_n - 1, withscores=True) return [(uid if isinstance(uid, str) else uid.decode("utf-8"), score) for uid, score in res] except Exception as e: logger.error(f"读取排行榜失败: {e}") return [] def leaderboard_realm_add(self, group_id: str, user_id: str, score: float): try: if not self.leaderboard_realm_key: return with self.get_redis() as r: r.zadd(f"{self.leaderboard_realm_key}:{group_id}", {user_id: score}) except Exception as e: logger.error(f"更新境界排行榜失败: {e}") def leaderboard_realm_top(self, group_id: str, top_n: int = 10) -> List[Tuple[str, float]]: try: if not self.leaderboard_realm_key: return [] with self.get_redis() as r: res = r.zrevrange(f"{self.leaderboard_realm_key}:{group_id}", 0, top_n - 1, withscores=True) return [(uid if isinstance(uid, str) else uid.decode("utf-8"), score) for uid, score in res] except Exception as e: logger.error(f"读取境界排行榜失败: {e}") return [] class XiuxianPlugin(MessagePluginInterface): """修仙主插件:指令分发、业务执行与 DB/Redis 协作。""" FEATURE_KEY = "XIUXIAN" FEATURE_DESCRIPTION = "🧙‍♂️ 文字修仙 [注册修仙|我的状态|闭关|出关|聚灵|排行榜]" @property def name(self) -> str: return "群聊文字修仙" @property def version(self) -> str: return "0.1.0" @property def description(self) -> str: return "基于Redis的简化修仙玩法,含闭关与状态机" @property def author(self) -> str: return "AI助手" @property def command_prefix(self) -> Optional[str]: return "" @property def commands(self) -> List[str]: return self._commands @property def feature_key(self) -> Optional[str]: return self.FEATURE_KEY @property def feature_description(self) -> Optional[str]: return self.FEATURE_DESCRIPTION def __init__(self): super().__init__() self.feature = self.register_feature() self.redis_db: Optional[XiuxianRedisDB] = None self.xdb: Optional[XiuxianDB] = None self.points_db: Optional[PointsDBOperator] = None self.revoke = None def initialize(self, context: Dict[str, Any]) -> bool: """初始化插件:加载配置,接入 DB 与 Redis,并注册功能权限。""" self.LOG = logger self.LOG.info(f"正在初始化 {self.name} 插件...") self.event_system = context.get("event_system") self.db_manager = DBConnectionManager.get_instance() if self.db_manager: self.redis_db = XiuxianRedisDB(self.db_manager) # 初始化持久化层(如连接池可用) if self.db_manager.mysql_pool: self.xdb = XiuxianDB(self.db_manager) self.points_db = PointsDBOperator(self.db_manager) try: self.xdb.init_schema() except Exception as e: self.LOG.error(f"修仙表结构初始化失败: {e}") cfg = self._config.get("Xiuxian", {}) self._commands = cfg.get("command", ["注册修仙", "我的状态", "闭关", "出关", "聚灵", "排行榜"]) self.command_format = cfg.get("command-format", "注册修仙 道号 | 我的状态 | 闭关 | 出关 | 聚灵 数量 | 排行榜") self.enable = cfg.get("enable", True) status_cfg = cfg.get("status", {}) self.unstable_qi_minutes = status_cfg.get("unstable_qi_minutes", 15) self.injured_minutes = status_cfg.get("injured_minutes", 60) self.max_cultivate_hours = status_cfg.get("max_cultivate_hours", 8) rate_cfg = cfg.get("rate_limit", {}) self.seconds_rl = { "我的状态": rate_cfg.get("status_seconds", 3), "闭关": rate_cfg.get("inout_seconds", 5), "出关": rate_cfg.get("inout_seconds", 5), "聚灵": rate_cfg.get("gather_seconds", 30), "排行榜": rate_cfg.get("break_seconds", 60), "注册修仙": 5, "签到": rate_cfg.get("signin_seconds", 86400), "坊市": rate_cfg.get("shop_seconds", 10), "购买": rate_cfg.get("buy_seconds", 5), "出售": rate_cfg.get("buy_seconds", 5), "乾坤袋": rate_cfg.get("bag_seconds", 3), "突破": rate_cfg.get("break_seconds", 60), "强行突破": rate_cfg.get("force_break_seconds", 60), "劫掠": rate_cfg.get("rob_seconds", 30), "赠与": rate_cfg.get("gift_seconds", 10), "赠送": rate_cfg.get("gift_seconds", 10), "创建门派": 86400, "加入门派": 604800, "退出门派": 604800, "积分购石": rate_cfg.get("points_to_stone_seconds", 10), "积分换灵石": rate_cfg.get("points_to_stone_seconds", 10), "出门历练": rate_cfg.get("expedition_seconds", 1800), "炼丹": rate_cfg.get("alchemy_seconds", 600), } cult_cfg = cfg.get("cultivation", {}) self.base_rate_per_hour = cult_cfg.get("base_rate_per_hour", 100) self.spirit_roots_cfg = cult_cfg.get("spirit_roots", ["凡灵根:1.0", "天灵根:2.0"]) self.spirit_roots = [] for item in self.spirit_roots_cfg: try: name, mult = item.split(":") self.spirit_roots.append((name, float(mult))) except Exception as e: self.LOG.warning(f"解析灵根配置失败: {item}, 错误: {e}") lb_cfg = cfg.get("leaderboard", {}) leaderboard_key = lb_cfg.get("key", "xiuxian:zset:leaderboard:cultivation") realm_lb_key = lb_cfg.get("realm_key", "xiuxian:zset:leaderboard:realm") if self.redis_db: self.redis_db.set_leaderboard_key(leaderboard_key) self.redis_db.set_realm_leaderboard_key(realm_lb_key) shop_cfg = cfg.get("shop", {}) self.shop_items = [] for s in shop_cfg.get("items", []): try: n, t, p = s.split(":") self.shop_items.append({"name": n, "type": t, "price": int(p)}) except Exception as e: self.LOG.warning(f"解析商品配置失败: {s}, 错误: {e}") materials_cfg = cfg.get("materials", {}) self.material_tier = {} self.materials_by_tier = {"T1": [], "T2": [], "T3": []} for m in materials_cfg.get("items", []): try: n, tier = m.split(":") self.material_tier[n] = tier if tier in self.materials_by_tier: self.materials_by_tier[tier].append(n) except Exception as e: self.LOG.warning(f"解析材料配置失败: {m}, 错误: {e}") recipes_cfg = cfg.get("recipes", {}) self.recipes = {} for r in recipes_cfg.get("items", []): try: name, rest = r.split(":") mats_part, stone_part, rate_part = rest.split(";") mats = {} for kv in mats_part.split(","): kv = kv.strip() if "*" in kv: mn, q = kv.split("*") elif "x" in kv: mn, q = kv.split("x") else: mn, q = kv, "1" mats[mn.strip()] = int(q.strip()) self.recipes[name] = {"materials": mats, "stone": int(stone_part.strip()), "rate": float(rate_part.strip())} except Exception as e: self.LOG.warning(f"解析配方失败: {r}, 错误: {e}") pts_cfg = cfg.get("points_exchange", {}) self.point_to_stone_rate = int(pts_cfg.get("point_to_stone_rate", 10)) # 统一解析境界配置:每层阈值、境界分值、突破规则 realms_cfg = self._config.get("Xiuxian", {}).get("realms", {}) if not realms_cfg: self.LOG.warning("未找到境界配置 [Xiuxian.realms],使用默认配置") # 提供默认配置作为后备 realms_cfg = { "炼气": "1000,10,筑基丹,0.4,0.1,2.0", "筑基": "5000,20,金元丹,0.2,0.05,2.0", "金丹": "50000,30,结婴丹,0.15,0.03,2.0", "元婴": "200000,40,化神丹,0.1,0.02,2.0", "化神": "1000000,50,合体丹,0.08,0.01,2.0", "合体": "5000000,60,大乘丹,0.05,0.005,2.0", "大乘": "10000000,70,渡劫丹,0.03,0.003,2.0", "渡劫": "50000000,80,飞升丹,0.02,0.001,2.0", "真仙": "0,100,,0,0,0" } self.realm_score_map = {} # 境界分值映射 self.layer_threshold_map = {} # 每层修为阈值 self.break_config = {} # 突破配置:{境界10层: {pill: {...}, hard: {...}}} # 境界顺序(用于计算下一个境界) realm_order = ["炼气", "筑基", "金丹", "元婴", "化神", "合体", "大乘", "渡劫", "真仙"] self.LOG.info(f"开始解析境界配置,共{len(realms_cfg)}个境界") for realm_name, config_str in realms_cfg.items(): try: # 解析配置:每层阈值,境界分值,突破丹药,丹药成功率,强行成功率,强行倍率 parts = config_str.split(",") if len(parts) < 6: self.LOG.warning(f"境界配置格式错误: {realm_name}={config_str}") continue layer_threshold = int(parts[0].strip()) realm_score = int(parts[1].strip()) pill_item = parts[2].strip() if parts[2].strip() else None pill_rate = float(parts[3].strip()) if parts[3].strip() else 0.0 hard_rate = float(parts[4].strip()) if parts[4].strip() else 0.0 hard_multiplier = float(parts[5].strip()) if parts[5].strip() else 2.0 # 存储层级阈值和境界分值 self.layer_threshold_map[realm_name] = layer_threshold self.realm_score_map[realm_name] = realm_score # 计算突破要求:达到10层需要9层×每层阈值 breakthrough_cost = 9 * layer_threshold hard_breakthrough_cost = int(breakthrough_cost * hard_multiplier) # 计算下一个境界 next_realm = None if realm_name != "真仙": try: current_idx = realm_order.index(realm_name) if current_idx + 1 < len(realm_order): next_realm = realm_order[current_idx + 1] except ValueError: pass # 构建突破配置 if next_realm and layer_threshold > 0: stage_key = f"{realm_name}10层" self.break_config[stage_key] = {} # 丹药突破路径 if pill_item and pill_rate > 0: self.break_config[stage_key]["pill"] = { "cost": breakthrough_cost, "rate": pill_rate, "target": f"{next_realm}1层", "item": pill_item } # 强行突破路径 if hard_rate > 0: self.break_config[stage_key]["hard"] = { "cost": hard_breakthrough_cost, "rate": hard_rate, "target": f"{next_realm}2层" } except Exception as e: self.LOG.warning(f"解析境界配置失败: {realm_name}={config_str}, 错误: {e}") self.LOG.info(f"境界配置解析完成:层级阈值{len(self.layer_threshold_map)}个,突破配置{len(self.break_config)}个") self.LOG.info(f"[{self.name}] 插件初始化完成,指令:{self._commands}") return True def start(self) -> bool: self.LOG.info(f"[{self.name}] 插件已启动") self.status = PluginStatus.RUNNING return True def stop(self) -> bool: self.LOG.info(f"[{self.name}] 插件已停止") self.status = PluginStatus.STOPPED return True def can_process(self, message: Dict[str, Any]) -> bool: if not self.enable: return False content = str(message.get("content", "")).strip() for cmd in self._commands: if content == cmd or content.startswith(f"{cmd} "): return True return False @plugin_stats_decorator(plugin_name="群聊文字修仙") @user_feature_rate_limit(max_per_minute=6, feature_key=FEATURE_KEY) async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: content = str(message.get("content", "")).strip() sender = message.get("sender") roomid = message.get("roomid", "") gbm: GroupBotManager = message.get("gbm") bot: WechatAPIClient = message.get("bot") self.revoke = message.get("revoke") if roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED: return False, "没有权限" cmd = None for c in self._commands: if content.startswith(c): cmd = c content = content[len(c):].strip() break if not cmd: return False, "不匹配的命令" if not self.redis_db: text = "❌ 天道失序,仙府未初始化(Redis)" + self._compose_status_text(sender, roomid) client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), text, sender) if self.revoke: self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "Redis未初始化" # 限流:默认按用户+群维度;历练按用户全局维度 if self.redis_db.check_rate_limited(sender, roomid or "", cmd): text = "⚠️ 道友莫急,天道有序,稍候再试" + self._compose_status_text(sender, roomid) client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), text, sender) if self.revoke: self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "限流" if cmd == "注册修仙": ok, msg = await self._cmd_register(bot, sender, roomid, content) return ok, msg if cmd in ("修仙帮助", "帮助", "help", "修仙指令", "指令"): ok, msg = await self._cmd_help(bot, sender, roomid) return ok, msg if cmd in ("积分购石", "积分换灵石"): ok, msg = await self._cmd_points_to_stone(bot, sender, roomid, content) return ok, msg if cmd == "我的状态": ok, msg = await self._cmd_status(bot, sender, roomid) return ok, msg if cmd == "闭关": ok, msg = await self._cmd_cultivate(bot, sender, roomid) return ok, msg if cmd == "出关": ok, msg = await self._cmd_finish_cultivate(bot, sender, roomid) return ok, msg if cmd == "聚灵": ok, msg = await self._cmd_gather(bot, sender, roomid, content) return ok, msg if cmd == "排行榜": ok, msg = await self._cmd_leaderboard(bot, sender, roomid) return ok, msg if cmd == "修仙签到": ok, msg = await self._cmd_signin(bot, sender, roomid) return ok, msg if cmd == "坊市": ok, msg = await self._cmd_shop(bot, sender, roomid) return ok, msg if cmd == "购买": ok, msg = await self._cmd_buy(bot, sender, roomid, content) return ok, msg if cmd == "出售": ok, msg = await self._cmd_sell(bot, sender, roomid, content) return ok, msg if cmd == "乾坤袋": ok, msg = await self._cmd_bag(bot, sender, roomid) return ok, msg if cmd in ("使用", "服用"): ok, msg = await self._cmd_use(bot, sender, roomid, content) return ok, msg if cmd == "出门历练": ok, msg = await self._cmd_expedition(bot, sender, roomid) return ok, msg if cmd == "炼丹": ok, msg = await self._cmd_alchemy(bot, sender, roomid, content) return ok, msg if cmd == "突破": ok, msg = await self._cmd_breakthrough(bot, sender, roomid) return ok, msg if cmd == "强行突破": ok, msg = await self._cmd_force_breakthrough(bot, sender, roomid) return ok, msg if cmd == "劫掠": ok, msg = await self._cmd_rob(bot, sender, roomid, content, message) return ok, msg if cmd == "赠与": ok, msg = await self._cmd_give_stone(bot, sender, roomid, content,message) return ok, msg if cmd == "赠送": ok, msg = await self._cmd_give_item(bot, sender, roomid, content, message) return ok, msg if cmd == "创建门派": ok, msg = await self._cmd_clan_create(bot, sender, roomid, content) return ok, msg if cmd == "加入门派": ok, msg = await self._cmd_clan_join(bot, sender, roomid, content) return ok, msg if cmd == "退出门派": ok, msg = await self._cmd_clan_exit(bot, sender, roomid) return ok, msg return False, "未知命令" def _rate_set(self, user_id: str, group_id: str, cmd: str): """设置用户维度 Redis 冷却键,用于防骚扰与防封。""" seconds = self.seconds_rl.get(cmd, 3) self.redis_db.set_rate_limit(user_id, group_id, cmd, seconds) def _rate_set_global(self, user_id: str, cmd: str): seconds = self.seconds_rl.get(cmd, 3) self.redis_db.set_rate_limit(user_id, "__global__", cmd, seconds) def _check_status_update(self, player: Dict[str, Any]) -> Dict[str, Any]: """状态机自动流转:过期的 Unstable_Qi/Injured 恢复为 Idle。""" now = datetime.now(timezone.utc) status = player.get("status", "Idle") until_str = player.get("status_until") until = None if until_str: try: until = datetime.fromisoformat(until_str) except Exception: until = None if until and until.tzinfo is None: until = until.replace(tzinfo=timezone.utc) if status in ("Unstable_Qi", "Injured", "Expedition") and until and now >= until: player["status"] = "Idle" player["status_until"] = None self._save_player(player) return player async def _cmd_register(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: dao_name = content.strip() if not dao_name: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), f"❌命令格式错误!\n{self.command_format}", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友施法有误,指令格式不对" player = self._get_player_with_cache(sender, roomid or "") if player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "⚠️ 已注册,无需重复注册", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) # self._rate_set(sender, "注册修仙") return True, "已注册" root_name, mult = random.choice(self.spirit_roots) if self.spirit_roots else ("凡灵根", 1.0) player = { "user_id": sender, "group_id": roomid or "", "dao_name": dao_name, "realm": "炼气1层", "spirit_root": f"{root_name}", "spirit_root_mult": mult, "cultivation_points": 0, "spirit_stone": 0, "status": "Idle", "status_until": None, "last_cultivate_time": None, } if self.xdb: try: self.xdb.create_player(player) except Exception as e: logger.warning(f"创建玩家数据库记录失败: {e}, user_id={sender}") self._save_player(player) self._rate_set(sender, roomid or "", "注册修仙") # 初始化境界排行榜分值 self.redis_db.leaderboard_realm_add(roomid or "", sender, float(self._realm_score(player["realm"]))) client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), f"✅ 入道已定,道号:{dao_name}\n灵根:{root_name}", sender) if self.revoke: self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 10) return True, "注册成功" async def _cmd_help(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: lines = ["📜 修仙帮助"] lines.append("用法提示:") lines.append(self.command_format.strip()) lines.append("可用指令:") lines.append(" | ".join(self._commands)) lines.append("常用参数格式:") lines.append("注册修仙 道号") lines.append("聚灵 数量") lines.append("购买 物品 数量") lines.append("出售 物品 数量") lines.append("出门历练") lines.append("炼丹 物品 数量") lines.append("使用 物品名") lines.append("服用 回气丹") lines.append("赠与 目标wxid 数量") lines.append("赠送 目标wxid 物品 数量") lines.append("劫掠 目标wxid") lines.append("创建门派 名称") lines.append("加入门派 名称") lines.append("积分购石 积分数") msg = "\n".join(lines) # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), msg, sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 30) # self._rate_set(sender, "帮助") return True, "帮助" async def _cmd_points_to_stone(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[ bool, str]: if not self.points_db: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "系统未初始化积分模块", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "积分体系未开,暂不可化石" player = self._get_player(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" try: pts = int(content.strip()) except Exception: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "命令格式:积分购石 积分数", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友施法有误,指令格式不对" if pts <= 0: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "积分数需为正整数", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友,数量需为正整数" group_id = roomid or "" ok, res = self.points_db.deduct_points(sender, group_id, pts, PointSource.PLUGIN, "修仙购买灵石") if not ok: cur = res.get("current_points", 0) # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, f"积分不足,当前积分:{cur}", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友积分不足" rate = int(self.point_to_stone_rate) if pts < rate: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, f"积分不足以兑换1灵石,至少需要{rate}积分", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友积分不足" stones_gain = pts // rate player["spirit_stone"] = int(player.get("spirit_stone", 0)) + stones_gain self._save_player(player) self._rate_set(sender, roomid or "", "积分购石") await self._send_text_with_status(bot, sender, roomid, f"✅ 积分化石,消耗{pts},得灵石{stones_gain}({rate}分=1石)", 90) return True, "积分购石" async def _cmd_status(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player_with_cache(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) status_code = player.get("status") status_cn = { "Idle": "空闲", "Cultivating": "闭关", "Unstable_Qi": "气息不稳", "Injured": "受伤保护", "Expedition": "历练" }.get(status_code, str(status_code)) msg = ( f"📇 道号:{player.get('dao_name')}【{player.get('spirit_root')}】\n" f"✨ 境界:{player.get('realm')}(修为:{player.get('cultivation_points')}点)\n" f"💎 灵石:{player.get('spirit_stone')}枚\n" f"💚 状态:{status_cn}" ) client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), msg, sender) if self.revoke: self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 20) self._rate_set(sender, roomid or "", "我的状态") return True, "状态展示" def _compose_status_text(self, sender: str, roomid: str) -> str: player = self._get_player_with_cache(sender, roomid or "") if not player: return "" player = self._check_status_update(player) status_code = player.get("status") status_cn = { "Idle": "空闲", "Cultivating": "闭关", "Unstable_Qi": "气息不稳", "Injured": "受伤保护", "Expedition": "历练" }.get(status_code, str(status_code)) return ( f"\n—— 当前状态 ——\n" f"📇 道号:{player.get('dao_name')}【{player.get('spirit_root')}】\n" f"✨ 境界:{player.get('realm')}(修为:{player.get('cultivation_points')}点)\n" f"💎 灵石:{player.get('spirit_stone')}枚\n" f"💚 状态:{status_cn}" ) async def _send_text_with_status(self, bot: WechatAPIClient, sender: str, roomid: str, main_text: str, revoke_seconds: int = 10): status_text = self._compose_status_text(sender, roomid) text = main_text + (status_text if status_text else "") client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, text, sender) if self.revoke: self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, revoke_seconds) async def _cmd_cultivate(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player_with_cache(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) status = player.get("status", "Idle") if status == "Cultivating": # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "⚠️ 已在闭关中", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "重复闭关" if status not in ("Idle", "Injured"): status_cn = { "Idle": "空闲", "Cultivating": "闭关", "Unstable_Qi": "气息不稳", "Injured": "受伤保护", "Expedition": "历练" }.get(status, str(status)) await self._send_text_with_status(bot, sender, roomid, f"当前状态[{status_cn}]不宜闭关", 90) return False, "状态不可闭关" player["status"] = "Cultivating" player["last_cultivate_time"] = datetime.now(timezone.utc).isoformat() self._save_player(player) self._rate_set(sender, roomid or "", "闭关") await self._send_text_with_status(bot, sender, roomid, "✅ 已入静闭关,期间天道护持,不可被劫", 90) return True, "闭关成功" async def _cmd_finish_cultivate(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player_with_cache(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) if player.get("status") != "Cultivating": # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "⚠️ 非闭关状态,无需出关", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "非闭关" start_iso = player.get("last_cultivate_time") try: start = datetime.fromisoformat(start_iso) if start_iso else datetime.now(timezone.utc) except Exception: start = datetime.now(timezone.utc) if start.tzinfo is None: start = start.replace(tzinfo=timezone.utc) now = datetime.now(timezone.utc) duration_hours = (now - start).total_seconds() / 3600.0 duration_hours = max(0.0, min(duration_hours, float(self.max_cultivate_hours))) mult = self._yield_multiplier(player.get("realm", "炼气1层")) rate = self.base_rate_per_hour * float(player.get("spirit_root_mult", 1.0)) * mult cbn = float(player.get("cultivate_bonus_next", 0.0) or 0.0) if cbn > 0: rate = rate * (1.0 + cbn) player["cultivate_bonus_next"] = 0.0 gain = int(duration_hours * rate) player["cultivation_points"] = int(player.get("cultivation_points", 0)) + gain player["status"] = "Unstable_Qi" player["status_until"] = (now + timedelta(minutes=int(self.unstable_qi_minutes))).isoformat() player["last_cultivate_time"] = None self._save_player(player) # 自动层级提升(不跨瓶颈) self._auto_layer_up(sender, player) self.redis_db.leaderboard_add(player.get("group_id", ""), sender, float(player["cultivation_points"])) self._rate_set(sender, roomid or "", "出关") await self._send_text_with_status(bot, sender, roomid, f"✅ 出关一朝,修为进益:{gain}({duration_hours:.1f}小时)\n现有修为:{player['cultivation_points']}\n状态:气息未平 {self.unstable_qi_minutes}分钟", 90) return True, "出关结算" async def _cmd_gather(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: player = self._get_player_with_cache(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) try: qty = int(content.strip()) except Exception: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), f"❌命令格式错误!\n{self.command_format}", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友施法有误,指令格式不对" stones = int(player.get("spirit_stone", 0)) if qty <= 0 or stones < qty: # client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "⚠️ 灵石不足或数量不合法", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "灵石不足" player["spirit_stone"] = stones - qty bonus = float(player.get("gather_bonus", 0.0)) debuff = float(player.get("gather_debuff", 0.0) or 0.0) gd_until = player.get("gather_debuff_until") if gd_until: try: u = datetime.fromisoformat(gd_until) if u and u.tzinfo is None: u = u.replace(tzinfo=timezone.utc) if not u or datetime.now(timezone.utc) >= u: debuff = 0.0 player["gather_debuff"] = 0.0 player["gather_debuff_until"] = None except Exception: debuff = 0.0 player["gather_debuff"] = 0.0 player["gather_debuff_until"] = None base_gain = qty * 10 mult = self._yield_multiplier(player.get("realm", "炼气1层")) factor = max(0.1, 1.0 + bonus + debuff) gain = int(base_gain * factor * mult) player["cultivation_points"] = int(player.get("cultivation_points", 0)) + gain if bonus > 0: player["gather_bonus"] = 0.0 self._save_player(player) # 自动层级提升(不跨瓶颈) self._auto_layer_up(sender, player) self.redis_db.leaderboard_add(player.get("group_id", ""), sender, float(player["cultivation_points"])) self._rate_set(sender, roomid or "", "聚灵") await self._send_text_with_status(bot, sender, roomid, f"✅ 灵气入体,消耗灵石{qty},修为涨{gain}", 90) return True, "聚灵成功" async def _cmd_leaderboard(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: top = self.redis_db.leaderboard_top(roomid or "", 50) items = [] for uid, score in top: player = self._get_player_with_cache(uid, roomid or "") if not player: continue cp = int(player.get("cultivation_points", int(score))) realm = player.get("realm", "炼气1层") rscore = self._realm_score(realm) root = player.get("spirit_root", "凡灵根") nick = player.get("dao_name") items.append((uid, nick or uid, root, realm, cp, rscore)) # 排序:先按境界分值降序,再按修为降序 items.sort(key=lambda x: (x[5], x[4]), reverse=True) lines = ["🏆 修仙榜 Top10"] for i, (_, nick, root, realm, cp, _) in enumerate(items[:10], start=1): lines.append(f"{i}. {nick}[{root}]-{realm}[{cp}]") await self._send_text_with_status(bot, sender, roomid, "\n".join(lines), 90) self._rate_set(sender, roomid or "", "排行榜") return True, "排行榜" def _get_player(self, user_id: str, group_id: str) -> Optional[Dict[str, Any]]: return self._get_player_with_cache(user_id, group_id) def _save_player(self, player: Dict[str, Any]): if self.xdb: fields = {} for k in ("group_id", "dao_name", "realm", "spirit_root", "cultivation_points", "spirit_stone", "status", "status_until", "last_cultivate_time", "clan_id"): if k in player: fields[k] = player[k] try: self.xdb.update_player_fields(player.get("user_id"), player.get("group_id", ""), fields) except Exception as e: logger.warning(f"更新玩家字段失败: {e}, user_id={player.get('user_id')}") self.redis_db.save_player(player) def _get_player_with_cache(self, user_id: str, group_id: str) -> Optional[Dict[str, Any]]: """从缓存或数据库获取玩家数据,并补充缺失字段""" p = self.redis_db.get_player(user_id, group_id) if p: return p if self.xdb: dbp = self.xdb.get_player(user_id, group_id) if dbp and dbp.get("group_id", "") == group_id: # 补充spirit_root_mult:根据spirit_root名称查找对应的倍率 if "spirit_root_mult" not in dbp: spirit_root_name = dbp.get("spirit_root", "凡灵根") mult = 1.0 for name, m in self.spirit_roots: if name == spirit_root_name: mult = m break dbp["spirit_root_mult"] = mult # 补充inventory:从数据库读取背包数据 if "inventory" not in dbp: try: items = self.xdb.get_inventory(user_id) dbp["inventory"] = {item["name"]: item["quantity"] for item in items} except Exception as e: logger.warning(f"读取背包数据失败: {e}") dbp["inventory"] = {} self.redis_db.save_player(dbp) return dbp return None def _parse_realm(self, realm: str) -> Tuple[str, Optional[int]]: try: if "层" in realm: prefix, layer_str = realm.split("层")[0], realm.split(" ")[-1] if " " in realm else None # expected format: 前缀N层,例如 炼气1层/筑基10层 import re m = re.match(r"^(.*?)?(\d+)层$", realm) if m: prefix = m.group(1) layer = int(m.group(2)) return prefix, layer except Exception: pass return realm, None def _realm_score(self, realm: str) -> int: prefix, layer = self._parse_realm(realm) base = self.realm_score_map.get(prefix, 0) return base + (layer or 0) def _yield_multiplier(self, realm: str) -> float: prefix, layer = self._parse_realm(realm) rs = self.realm_score_map.get(prefix, 0) realm_mult = 1.0 + (float(rs) / 100.0) layer_bonus = 1.0 + (max(0, (layer or 1) - 1) * 0.02) return realm_mult * layer_bonus def _set_realm(self, user_id: str, player: Dict[str, Any], new_realm: str): player["realm"] = new_realm if self.xdb: try: self.xdb.update_player_fields(user_id, player.get("group_id", ""), {"realm": new_realm}) self.redis_db.invalidate_player(user_id, player.get("group_id", "")) except Exception: pass self._save_player(player) # 更新境界排行榜 self.redis_db.leaderboard_realm_add(player.get("group_id", ""), user_id, float(self._realm_score(new_realm))) def _auto_layer_up(self, user_id: str, player: Dict[str, Any]): """自动层级提升:根据修为自动提升层数(不跨瓶颈) 逻辑: - 炼气1层:0-999修为 - 炼气2层:1000-1999修为 - ... - 炼气10层:9000-9999修为 计算公式:new_layer = min(10, (pts // threshold) + 1) """ prefix, layer = self._parse_realm(player.get("realm", "凡人")) if layer is None or layer >= 10: return threshold = self.layer_threshold_map.get(prefix) if not threshold: return pts = int(player.get("cultivation_points", 0)) # 修复:计算应该达到的层数 = (修为 // 每层阈值) + 1 # 例如:1000修为 → 1000 // 1000 = 1 → 1 + 1 = 2层 calculated_layer = min(10, (pts // threshold) + 1) # 只允许提升,不允许降低 new_layer = max(layer, calculated_layer) if new_layer != layer: self._set_realm(user_id, player, f"{prefix}{new_layer}层") async def _cmd_signin(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) if self.redis_db.check_rate_limited(sender, roomid or "", "签到"): # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "今日已签到,请明日再来", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "已签到" reward = 50 player["spirit_stone"] = int(player.get("spirit_stone", 0)) + reward self._save_player(player) self._rate_set(sender, roomid or "", "签到") await self._send_text_with_status(bot, sender, roomid, f"✅ 灵运昌盛,获灵石{reward}", 90) return True, "签到成功" async def _cmd_shop(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: lines = ["🛒 坊市陈列"] for item in self.shop_items: lines.append(f"{item['name']} [{item['type']}] - {item['price']}灵石") await self._send_text_with_status(bot, sender, roomid, "\n".join(lines), 90) self._rate_set(sender, roomid or "", "坊市") return True, "坊市" async def _cmd_buy(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: parts = content.split() if len(parts) < 2: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, f"❌命令格式错误!\n购买 物品 数量", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友施法有误,指令格式不对" item_name = parts[0] try: qty = int(parts[1]) except Exception: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, f"❌命令格式错误!\n购买 物品 数量", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友施法有误,指令格式不对" player = self._get_player(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) item = next((i for i in self.shop_items if i["name"] == item_name), None) if not item: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "商品不存在", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "坊市中无此物" total = item["price"] * qty stones = int(player.get("spirit_stone", 0)) if qty <= 0 or stones < total: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "灵石不足", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "灵石不够,修行不可强行为之" player["spirit_stone"] = stones - total inv = player.get("inventory") or {} inv[item_name] = int(inv.get(item_name, 0)) + qty player["inventory"] = inv if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"spirit_stone": player["spirit_stone"]}) self.xdb.add_item(sender, item_name, item.get("type", "other"), qty) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "购买") await self._send_text_with_status(bot, sender, roomid, f"✅ 已购得 {item_name} × {qty}", 90) return True, "购买成功" async def _cmd_sell(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: parts = content.split() if len(parts) < 2: return False, "道友施法有误,指令格式不对" item_name = parts[0] try: qty = int(parts[1]) except Exception: return False, "道友施法有误,指令格式不对" player = self._get_player(sender, roomid or "") if not player: return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) inv = player.get("inventory") or {} have = int(inv.get(item_name, 0)) if qty <= 0 or have < qty: return False, "乾坤袋中此物不足" item = next((i for i in self.shop_items if i["name"] == item_name), None) if not item: return False, "坊市中无此物" sell_ratio = 0.5 revenue = int(item["price"] * qty * sell_ratio) inv[item_name] = have - qty player["inventory"] = inv player["spirit_stone"] = int(player.get("spirit_stone", 0)) + revenue if self.xdb: try: self.xdb.remove_item(sender, item_name, qty) self.xdb.update_player_fields(sender, player.get("group_id", ""), {"spirit_stone": player["spirit_stone"]}) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "出售") await self._send_text_with_status(bot, sender, roomid, f"✅ 已出手 {item_name} × {qty},入账灵石{revenue}", 90) return True, "出售成功" async def _cmd_bag(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" inv = player.get("inventory") or {} items = [] if self.xdb: try: items = self.xdb.get_inventory(sender) except Exception: items = [] lines = ["🎒 背包"] if not items: lines.append("空") else: for it in items: lines.append(f"{it['name']} × {it['quantity']}") await self._send_text_with_status(bot, sender, roomid, "\n".join(lines), 90) self._rate_set(sender, roomid or "", "背包") return True, "背包" async def _cmd_expedition(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player(sender, roomid or "") if not player: return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" if self.redis_db.check_rate_limited(sender, "__global__", "出门历练"): client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender), "⚠️ 天机不可泄,今日行程已定,稍候再试", sender) if self.revoke: self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "限流" player = self._check_status_update(player) st = player.get("status", "Idle") if st in ("Cultivating", "Injured", "Expedition"): return False, "状态不可历练" now = datetime.now(timezone.utc) player["status"] = "Expedition" player["status_until"] = (now + timedelta(minutes=20)).isoformat() self._save_player(player) self._rate_set_global(sender, "出门历练") prefix, layer = self._parse_realm(player.get("realm", "炼气1层")) rs = self.realm_score_map.get(prefix, 0) mult = self._yield_multiplier(player.get("realm", "炼气1层")) orig_stones = int(player.get("spirit_stone", 0)) stones = orig_stones mats_gain: Dict[str, int] = {} cult_gain = 0 final_status = "Expedition" final_until = player.get("status_until") events = [ "洞府遗址", "古老传承", "天材地宝", "大妖横行", "魔头埋伏", "天劫试炼", "禁地余威", "灵脉枯竭", "灵潮涌动", ] weights = [ 30 + rs, 20 + rs, 25 + max(rs - 10, 0), 25, 20, 15 + max(rs - 20, 0), 15, 10, 10 + rs, ] count = 1 if prefix in ("筑基", "金丹"): count = 2 elif prefix in ("元婴", "化神", "合体", "大乘", "渡劫", "真仙"): count = random.choice([2, 3]) happened = [] for _ in range(count): idx = random.choices(range(len(events)), weights=weights, k=1)[0] name = events[idx] happened.append(name) if name == "洞府遗址": stones += int(random.uniform(80, 200) * mult) n = random.randint(2, 4) tiers = ["T3", "T2"] if rs >= 40 else ["T2", "T1"] for _m in range(n): tier = tiers[0] if random.random() < 0.5 else tiers[1] pool = self.materials_by_tier.get(tier, []) if pool: mk = random.choice(pool) mats_gain[mk] = mats_gain.get(mk, 0) + 1 if random.random() < 0.25: final_status = "Unstable_Qi" final_until = (datetime.now(timezone.utc) + timedelta(minutes=20)).isoformat() elif name == "古老传承": gain = int(random.uniform(100, 500) * mult) cult_gain += gain player["cultivate_bonus_next"] = float(player.get("cultivate_bonus_next", 0.0) or 0.0) + 0.1 elif name == "天材地宝": stones += int(random.uniform(30, 80) * mult) n = random.randint(1, 3) tiers = ["T2", "T1"] for _m in range(n): tier = tiers[0] if random.random() < min(0.3 * mult, 0.7) else tiers[1] pool = self.materials_by_tier.get(tier, []) if pool: mk = random.choice(pool) mats_gain[mk] = mats_gain.get(mk, 0) + 1 if rs >= 50 and random.random() < 0.3: pool = self.materials_by_tier.get("T3", []) if pool: mk = random.choice(pool) mats_gain[mk] = mats_gain.get(mk, 0) + 1 elif name == "大妖横行": p = min(0.5 + rs / 200.0, 0.8) if random.random() < p: gain = int(max(0, stones) * random.uniform(0.2, 0.5)) stones += gain n = random.randint(2, 5) pool = self.materials_by_tier.get("T2", []) for _m in range(n): if pool: mk = random.choice(pool) mats_gain[mk] = mats_gain.get(mk, 0) + 1 if rs >= 50 and random.random() < 0.2: pool3 = self.materials_by_tier.get("T3", []) if pool3: mk = random.choice(pool3) mats_gain[mk] = mats_gain.get(mk, 0) + 1 else: loss = int(max(0, stones) * random.uniform(0.1, 0.3)) stones = max(0, stones - loss) final_status = "Injured" final_until = (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat() elif name == "魔头埋伏": loss = int(max(0, stones) * random.uniform(0.2, 0.4)) stones = max(0, stones - loss) if random.random() < 0.3: inv = player.get("inventory") or {} keys = [k for k, v in inv.items() if int(v) > 0] if keys: rk = random.choice(keys) inv[rk] = max(0, int(inv.get(rk, 0)) - 1) player["inventory"] = inv player["gather_debuff"] = -random.uniform(0.1, 0.2) player["gather_debuff_until"] = (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat() elif name == "天劫试炼": if rs >= 40 and random.random() < 0.4: gain = int(random.uniform(200, 800) * mult) cult_gain += gain else: final_status = "Injured" final_until = (datetime.now(timezone.utc) + timedelta(minutes=30)).isoformat() elif name == "禁地余威": final_status = "Unstable_Qi" final_until = (datetime.now(timezone.utc) + timedelta(minutes=random.randint(20, 30))).isoformat() elif name == "灵脉枯竭": pass elif name == "灵潮涌动": player["gather_bonus"] = float(player.get("gather_bonus", 0.0) or 0.0) + 0.2 player["spirit_stone"] = stones if cult_gain > 0: player["cultivation_points"] = int(player.get("cultivation_points", 0)) + cult_gain self.redis_db.leaderboard_add(player.get("group_id", ""), sender, float(player["cultivation_points"])) inv = player.get("inventory") or {} for k, v in mats_gain.items(): inv[k] = int(inv.get(k, 0)) + v player["inventory"] = inv if final_status in ("Injured", "Unstable_Qi"): player["status"] = final_status player["status_until"] = final_until if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"spirit_stone": player["spirit_stone"], "cultivation_points": player.get("cultivation_points", 0), "status": player.get("status"), "status_until": player.get("status_until")}) for k, v in mats_gain.items(): self.xdb.add_item(sender, k, "材料", v) except Exception: pass self._save_player(player) self._rate_set_global(sender, "出门历练") mats_text = ", ".join([f"{k}×{v}" for k, v in mats_gain.items()]) if mats_gain else "无" total_stones_delta = player["spirit_stone"] - orig_stones if cult_gain > 0: msg = f"✅ 行走四方所得:灵石{total_stones_delta},修为{cult_gain},材料:{mats_text}\n遭遇:{'、'.join(happened)}" else: msg = f"✅ 行走四方所得:灵石{total_stones_delta},材料:{mats_text}\n遭遇:{'、'.join(happened)}" await self._send_text_with_status(bot, sender, roomid, msg, 90) return True, "历练成功" async def _cmd_use(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: item_name = content.strip() if not item_name: return False, "道友施法有误,指令格式不对" player = self._get_player(sender, roomid or "") if not player: return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) inv = player.get("inventory") or {} qty = int(inv.get(item_name, 0)) if qty <= 0: return False, "物品不足" if item_name == "聚灵符": inv[item_name] = qty - 1 player["inventory"] = inv player["gather_bonus"] = float(player.get("gather_bonus", 0.0)) + 0.2 if self.xdb: try: self.xdb.remove_item(sender, item_name, 1) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "使用") await self._send_text_with_status(bot, sender, roomid, "✅ 符文已启,本次聚灵更为顺畅", 90) return True, "使用聚灵符" if item_name == "回气丹": status = player.get("status", "Idle") if status not in ("Unstable_Qi", "Injured"): return False, "当前状态无需服用" inv[item_name] = qty - 1 player["inventory"] = inv player["status"] = "Idle" player["status_until"] = None if self.xdb: try: self.xdb.remove_item(sender, item_name, 1) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "使用") await self._send_text_with_status(bot, sender, roomid, "✅ 服下回气丹,气息平稳,恢复空闲", 90) return True, "服用回气丹" # 灵根提升丹药 upgrade_map = { "洗髓丹": {"from": "废灵根", "to": "凡灵根", "rate": 0.6, "injured_min": 30}, "改灵丹": {"from": "凡灵根", "to": "地灵根", "rate": 0.3, "injured_min": 60}, "天灵露": {"from": "地灵根", "to": "天灵根", "rate": 0.1, "injured_min": 120}, } if item_name in upgrade_map: cur_root = player.get("spirit_root", "凡灵根") rule = upgrade_map[item_name] if cur_root != rule["from"]: return False, "当前灵根不适用" # 扣减丹药 inv[item_name] = qty - 1 player["inventory"] = inv if self.xdb: try: self.xdb.remove_item(sender, item_name, 1) except Exception: pass roll = random.random() if roll < rule["rate"]: # 成功,更新灵根与倍率 target = rule["to"] player["spirit_root"] = target mult = 1.0 for name, m in self.spirit_roots: if name == target: mult = m break player["spirit_root_mult"] = mult self._save_player(player) self._rate_set(sender, roomid or "", "使用") await self._send_text_with_status(bot, sender, roomid, f"✅ 灵根蜕变,当前灵根:{target}", 90) return True, "灵根提升成功" else: # 失败,受伤保护 player["status"] = "Injured" player["status_until"] = ( datetime.now(timezone.utc) + timedelta(minutes=rule["injured_min"])).isoformat() self._save_player(player) self._rate_set(sender, roomid or "", "使用") await self._send_text_with_status(bot, sender, roomid, f"❌ 灵根淬炼失利,灵气反噬,受伤{rule['injured_min']}分钟", 90) return False, "灵根提升失败" return False, "不可使用的物品" async def _cmd_alchemy(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: parts = content.split() if len(parts) < 2: return False, "道友施法有误,指令格式不对" item_name = parts[0] try: qty = int(parts[1]) except Exception: return False, "道友施法有误,指令格式不对" player = self._get_player(sender, roomid or "") if not player: return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) recipe = self.recipes.get(item_name) if not recipe: return False, "此丹方不传外人,尚未掌握" total_stone = recipe["stone"] * qty if int(player.get("spirit_stone", 0)) < total_stone: return False, "灵石不够,修行不可强行为之" inv = player.get("inventory") or {} need = {} for mk, mv in recipe["materials"].items(): need[mk] = mv * qty for mk, nv in need.items(): if int(inv.get(mk, 0)) < nv: return False, "乾坤袋中材料不足" for mk, nv in need.items(): inv[mk] = int(inv.get(mk, 0)) - nv success = 0 fail = 0 item_type = next((i["type"] for i in self.shop_items if i["name"] == item_name), "丹药") for _ in range(qty): if random.random() < float(recipe["rate"]): success += 1 else: fail += 1 player["spirit_stone"] = int(player.get("spirit_stone", 0)) - total_stone inv[item_name] = int(inv.get(item_name, 0)) + success player["inventory"] = inv if fail > 0: player["status"] = "Unstable_Qi" player["status_until"] = ( datetime.now(timezone.utc) + timedelta(minutes=int(self.unstable_qi_minutes))).isoformat() if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"spirit_stone": player["spirit_stone"], "status": player.get("status"), "status_until": player.get("status_until")}) if success > 0: self.xdb.add_item(sender, item_name, item_type, success) for mk, nv in need.items(): if nv > 0: self.xdb.remove_item(sender, mk, nv) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "炼丹") msg = f"✅ 丹炉开盖,成丹{success},走丹{fail}" await self._send_text_with_status(bot, sender, roomid, msg, 90) return True, "炼丹完成" async def _cmd_breakthrough(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) points = int(player.get("cultivation_points", 0)) # 读取当前瓶颈配置 cur_realm = player.get("realm", "炼气1层") prefix, layer = self._parse_realm(cur_realm) # 优化:检查是否真的达到10层 if layer is None or layer < 10: await self._send_text_with_status(bot, sender, roomid, f"当前境界:{cur_realm},唯有至{prefix}10层方可冲关", 90) return False, "未到瓶颈" stage_key = f"{prefix}10层" stage_conf = self.break_config.get(stage_key) if not stage_conf: await self._send_text_with_status(bot, sender, roomid, "此境瓶颈暂未开示,无法突破", 90) return False, "未配置突破" pill_conf = stage_conf.get("pill") if not pill_conf: await self._send_text_with_status(bot, sender, roomid, "丹道途径未明,暂不可试", 90) return False, "未配置" # 从配置中获取丹药名称 pill_item_name = pill_conf.get("item") if not pill_item_name: await self._send_text_with_status(bot, sender, roomid, "缺少突破丹药的指引", 90) return False, "丹药未配置" # 优化:从数据库读取最新的背包数据,确保数据准确性 pill_item_count = 0 if self.xdb: try: items = self.xdb.get_inventory(sender) for item in items: if item["name"] == pill_item_name: pill_item_count = item["quantity"] break except Exception as e: logger.warning(f"读取背包数据失败: {e}, user_id={sender}") # 降级到使用内存中的inventory inv = player.get("inventory") or {} pill_item_count = inv.get(pill_item_name, 0) else: inv = player.get("inventory") or {} pill_item_count = inv.get(pill_item_name, 0) if points < pill_conf["cost"]: await self._send_text_with_status(bot, sender, roomid, "底蕴未满,暂不可冲关", 90) return False, "修为不足" if pill_item_count <= 0: await self._send_text_with_status(bot, sender, roomid, f"丹药未备:{pill_item_name}", 90) return False, "缺少丹药" # 更新内存中的inventory(用于缓存) inv = player.get("inventory") or {} inv[pill_item_name] = pill_item_count - 1 player["inventory"] = inv player["cultivation_points"] = points - pill_conf["cost"] if self.xdb: try: self.xdb.remove_item(sender, pill_item_name, 1) self.xdb.update_player_fields(sender, player.get("group_id", ""), {"cultivation_points": player["cultivation_points"]}) self.redis_db.invalidate_player(sender, player.get("group_id", "")) except Exception as e: logger.warning(f"突破时更新数据库失败: {e}, user_id={sender}") roll = random.random() if roll < pill_conf["rate"]: # 成功,更新境界并排行榜 self._set_realm(sender, player, pill_conf["target"]) self.redis_db.leaderboard_add(player.get("group_id", ""), sender, float(player["cultivation_points"])) self._rate_set(sender, roomid or "", "突破") await self._send_text_with_status(bot, sender, roomid, f"✅ 雷霆破关,晋至{pill_conf['target']}", 90) return True, "突破成功" else: # 失败时也要保存玩家数据 self._save_player(player) self._rate_set(sender, roomid or "", "突破") await self._send_text_with_status(bot, sender, roomid, "❌ 闭关未成,功亏一篑", 90) return False, "突破失败" async def _cmd_force_breakthrough(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" player = self._check_status_update(player) points = int(player.get("cultivation_points", 0)) # 读取当前瓶颈配置 cur_realm = player.get("realm", "炼气1层") prefix, layer = self._parse_realm(cur_realm) # 优化:检查是否真的达到10层 if layer is None or layer < 10: await self._send_text_with_status(bot, sender, roomid, f"当前境界:{cur_realm},至{prefix}10层方能冒险强行冲关", 90) return False, "未到瓶颈" stage_key = f"{prefix}10层" stage_conf = self.break_config.get(stage_key) if not stage_conf: await self._send_text_with_status(bot, sender, roomid, "此境瓶颈暂未开示,无法强行冲关", 90) return False, "未配置突破" hard_conf = stage_conf.get("hard") if not hard_conf: await self._send_text_with_status(bot, sender, roomid, "强行之法未载,难以施为", 90) return False, "未配置" if points < hard_conf["cost"]: await self._send_text_with_status(bot, sender, roomid, "底蕴不足,强行为之只会贻害", 90) return False, "修为不足" player["cultivation_points"] = points - hard_conf["cost"] if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"cultivation_points": player["cultivation_points"]}) self.redis_db.invalidate_player(sender, player.get("group_id", "")) except Exception as e: logger.warning(f"强行突破时更新数据库失败: {e}, user_id={sender}") roll = random.random() if roll < hard_conf["rate"]: self._set_realm(sender, player, hard_conf["target"]) self.redis_db.leaderboard_add(player.get("group_id", ""), sender, float(player["cultivation_points"])) self._rate_set(sender, roomid or "", "强行突破") await self._send_text_with_status(bot, sender, roomid, f"✅ 硬撼瓶颈,勉强晋至{hard_conf['target']}", 90) return True, "强行突破成功" else: # 失败时也要保存玩家数据 self._save_player(player) self._rate_set(sender, roomid or "", "强行突破") await self._send_text_with_status(bot, sender, roomid, "❌ 强行冲关失败,灵气反噬!", 90) return False, "强行突破失败" async def _cmd_rob(self, bot: WechatAPIClient, sender: str, roomid: str, content: str, message: Dict[str, Any]) -> Tuple[bool, str]: msg = message.get("full_wx_msg") xml = getattr(msg, "msg_source", None) if msg else None target_wxid = None if roomid and xml: try: import xml.etree.ElementTree as ET import re root = ET.fromstring(xml) at_el = root.find('.//atuserlist') text = (at_el.text if at_el is not None else '').strip() text = text.strip(',') text = re.sub(r'[\s\u3000]+', '', text) at_set = set(text.split(',')) at_set.discard('') if len(at_set) != 1: await self._send_text_with_status(bot, sender, roomid, "请@一个明确的目标进行劫掠", 90) return False, "目标无效" target_wxid = next(iter(at_set)) except Exception: pass if target_wxid == sender: return False, "不可劫掠自己" attacker = self._get_player(sender, roomid or "") defender = self._get_player(target_wxid, roomid or "") if not attacker or not defender: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "双方需已注册", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" attacker = self._check_status_update(attacker) defender = self._check_status_update(defender) # 新手保护:不可劫掠炼气期 def_prefix, _ = self._parse_realm(defender.get("realm", "炼气1层")) if def_prefix == "炼气": await self._send_text_with_status(bot, sender, roomid, "对方新入仙途,天道庇护,暂不可劫", 90) return False, "新手保护" if defender.get("status") in ("Cultivating", "Injured"): await self._send_text_with_status(bot, sender, roomid, "对方正受天道护持或闭关参悟,切莫打扰", 90) return False, "目标保护" if roomid and (attacker.get("group_id") != roomid or defender.get("group_id") != roomid): await self._send_text_with_status(bot, sender, roomid, "只可在同一仙门之境内行劫", 90) return False, "跨群" if attacker.get("clan_id") and defender.get("clan_id") and attacker.get("clan_id") == defender.get("clan_id"): await self._send_text_with_status(bot, sender, roomid, "同门手足,不可相互劫掠", 90) return False, "同门" a_pts = int(attacker.get("cultivation_points", 0)) d_pts = int(defender.get("cultivation_points", 0)) base = 0.5 if a_pts > d_pts: base += 0.2 elif a_pts < d_pts: base -= 0.2 roll = random.random() if roll < base: d_stones = int(defender.get("spirit_stone", 0)) gain = max(0, int(d_stones * 0.2)) defender["spirit_stone"] = d_stones - gain attacker["spirit_stone"] = int(attacker.get("spirit_stone", 0)) + gain defender["status"] = "Injured" defender["status_until"] = ( datetime.now(timezone.utc) + timedelta(minutes=int(self.injured_minutes))).isoformat() if self.xdb: try: self.xdb.update_player_fields(defender.get("user_id"), defender.get("group_id", ""), {"spirit_stone": defender["spirit_stone"], "status": defender["status"], "status_until": defender["status_until"]}) self.xdb.update_player_fields(attacker.get("user_id"), attacker.get("group_id", ""), {"spirit_stone": attacker["spirit_stone"]}) except Exception: pass self._save_player(defender) self._save_player(attacker) self._rate_set(sender, roomid or "", "劫掠") dao_name = defender.get("dao_name", "") await self._send_text_with_status(bot, sender, roomid, f"✅ 劫掠得手,入账灵石{gain}", 90) if roomid: g_client_msg_id, g_create_time, g_new_msg_id = await bot.send_text_message(roomid, f"{sender} 劫掠 {dao_name} 得手,目标陷入重创,暂受天道庇护", [target_wxid]) if self.revoke: self.revoke.add_message_to_revoke(roomid, g_client_msg_id, g_create_time, g_new_msg_id, 90) return True, "劫掠成功" else: self._rate_set(sender, roomid or "", "劫掠") await self._send_text_with_status(bot, sender, roomid, "❌ 劫掠受挫,风声紧,暂且退去", 90) return False, "劫掠失败" async def _cmd_give_stone(self, bot: WechatAPIClient, sender: str, roomid: str, content: str, message: Dict[str, Any]) -> Tuple[bool, str]: msg = message.get("full_wx_msg") xml = getattr(msg, "msg_source", None) if msg else None target_wxid = None if roomid and xml: try: import xml.etree.ElementTree as ET import re root = ET.fromstring(xml) at_el = root.find('.//atuserlist') text = (at_el.text if at_el is not None else '').strip() text = text.strip(',') text = re.sub(r'\s+', '', text) at_set = set(text.split(',')) at_set.discard('') if len(at_set) != 1: await self._send_text_with_status(bot, sender, roomid, "请@一个明确的目标进行赠送", 90) return False, "目标无效" target_wxid = next(iter(at_set)) tokens = [t for t in content.strip().split() if not t.startswith('@')] if len(tokens) < 1: return False, "道友施法有误,指令格式不对" try: qty = int(tokens[0]) except Exception: return False, "道友施法有误,指令格式不对" except Exception: pass if not target_wxid: parts = content.strip().split() if len(parts) < 2: await self._send_text_with_status(bot, sender, roomid, "格式:赠与 目标 数量", 90) return False, "命令格式错误" parts = content.strip().split() if len(parts) < 2: await self._send_text_with_status(bot, sender, roomid, "格式:赠与 目标 数量", 90) return False, "命令格式错误" try: qty = int(parts[1]) except Exception: await self._send_text_with_status(bot, sender, roomid, "格式:赠与 目标 数量", 90) return False, "命令格式错误" giver = self._get_player_with_cache(sender, roomid or "") receiver = self._get_player_with_cache(target_wxid, roomid or "") if not giver or not receiver: await self._send_text_with_status(bot, sender, roomid, "需双方皆踏入仙途", 90) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" stones = int(giver.get("spirit_stone", 0)) if qty <= 0 or stones < qty: await self._send_text_with_status(bot, sender, roomid, "灵石不足或数量有误", 90) return False, "灵石不够,修行不可强行为之" giver["spirit_stone"] = stones - qty receiver["spirit_stone"] = int(receiver.get("spirit_stone", 0)) + qty if self.xdb: try: self.xdb.update_player_fields(sender, giver.get("group_id", ""), {"spirit_stone": giver["spirit_stone"]}) self.xdb.update_player_fields(target_wxid, receiver.get("group_id", ""), {"spirit_stone": receiver["spirit_stone"]}) except Exception: pass # 使用_save_player确保同时保存到Redis和MariaDB self._save_player(giver) self._save_player(receiver) self._rate_set(sender, roomid or "", "赠与") dao_name =receiver.get("dao_name", "") await self._send_text_with_status(bot, sender, roomid, f"✅ 已向 {dao_name} 相赠灵石 {qty}", 90) return True, "赠与成功" async def _cmd_give_item(self, bot: WechatAPIClient, sender: str, roomid: str, content: str, message: Dict[str, Any]) -> Tuple[bool, str]: parts = content.strip().split() if len(parts) < 1: return False, "道友施法有误,指令格式不对" msg = message.get("full_wx_msg") xml = getattr(msg, "msg_source", None) if msg else None target_wxid = None item_name = None qty = 0 if roomid and xml: try: import xml.etree.ElementTree as ET import re root = ET.fromstring(xml) at_el = root.find('.//atuserlist') text = (at_el.text if at_el is not None else '').strip() text = text.strip(',') text = re.sub(r'\s+', '', text) at_set = set(text.split(',')) at_set.discard('') if len(at_set) != 1: await self._send_text_with_status(bot, sender, roomid, "请@一个明确的目标进行赠送", 90) return False, "目标无效" target_wxid = next(iter(at_set)) tokens = [t for t in parts if not t.startswith('@')] if len(tokens) < 2: return False, "道友施法有误,指令格式不对" item_name = tokens[0] try: qty = int(tokens[1]) except Exception: return False, "道友施法有误,指令格式不对" except Exception: pass if not target_wxid: if len(parts) < 3: return False, "道友施法有误,指令格式不对" target_wxid = parts[0].lstrip('@') item_name = parts[1] try: qty = int(parts[2]) except Exception: return False, "道友施法有误,指令格式不对" giver = self._get_player(sender, roomid or "") receiver = self._get_player(target_wxid, roomid or "") if not giver or not receiver: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "双方需已注册", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" inv_g = giver.get("inventory") or {} if inv_g.get(item_name, 0) < qty or qty <= 0: await self._send_text_with_status(bot, sender, roomid, "乾坤袋物品不足或数量有误", 90) return False, "物品不足" inv_r = receiver.get("inventory") or {} inv_g[item_name] = inv_g.get(item_name, 0) - qty inv_r[item_name] = int(inv_r.get(item_name, 0)) + qty giver["inventory"] = inv_g receiver["inventory"] = inv_r if self.xdb: ok = self.xdb.transfer_item(sender, target_wxid, item_name, qty) if not ok: await self._send_text_with_status(bot, sender, roomid, "物品转移不成", 90) return False, "转移失败" self.redis_db.invalidate_player(sender, giver.get("group_id", "")) self.redis_db.invalidate_player(target_wxid, receiver.get("group_id", "")) self._save_player(giver) self._save_player(receiver) self._rate_set(sender, roomid or "", "赠送") dao_name =receiver.get("dao_name", "") await self._send_text_with_status(bot, sender, roomid, f"✅ 已向 {dao_name} 赠送 {item_name} × {qty}", 90) return True, "赠送成功" async def _cmd_clan_create(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: name = content.strip() if not name: await self._send_text_with_status(bot, sender, roomid, "格式:创建门派 名称", 90) return False, "命令格式错误" player = self._get_player_with_cache(sender, roomid or "") if not player: # client_msg_id, create_time, new_msg_id = await bot.send_text_message(roomid or sender, "未注册,请先发送:注册修仙 道号", sender) # if self.revoke: # self.revoke.add_message_to_revoke((roomid if roomid else sender), client_msg_id, create_time, new_msg_id, 5) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" prefix, _ = self._parse_realm(player.get("realm", "炼气1层")) allowed = {"元婴", "化神", "合体", "大乘", "渡劫", "真仙"} if prefix not in allowed: await self._send_text_with_status(bot, sender, roomid, "唯元婴以上方可立宗开派", 90) return False, "境界不足" clan_id = None if self.xdb: clan_id = self.xdb.create_clan(name, roomid or "", sender) if clan_id is None: await self._send_text_with_status(bot, sender, roomid, "此门已在或因故未成", 90) return False, "门派失败" if player and clan_id is not None: player["clan_id"] = int(clan_id) if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"clan_id": player["clan_id"]}) self.redis_db.invalidate_player(sender, player.get("group_id", "")) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "创建门派") await self._send_text_with_status(bot, sender, roomid, f"✅ 山门立成:{name}", 90) return True, "创建门派" async def _cmd_clan_join(self, bot: WechatAPIClient, sender: str, roomid: str, content: str) -> Tuple[bool, str]: name = content.strip() if not name: await self._send_text_with_status(bot, sender, roomid, "格式:加入门派 名称", 90) return False, "命令格式错误" player = self._get_player_with_cache(sender, roomid or "") if not player: await self._send_text_with_status(bot, sender, roomid, "道友尚未踏入仙途,请先发送:注册修仙 道号", 90) return False, "道友尚未踏入仙途,请先发送:注册修仙 道号" cid = None if self.xdb: cid = self.xdb.get_clan_id(roomid or "", name) if not cid: await self._send_text_with_status(bot, sender, roomid, "世间无此门派", 90) return False, "门派不存在" player["clan_id"] = int(cid) if isinstance(cid, str) else cid if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"clan_id": player["clan_id"]}) self.redis_db.invalidate_player(sender, player.get("group_id", "")) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "加入门派") await self._send_text_with_status(bot, sender, roomid, f"✅ 已拜入:{name}", 90) return True, "加入门派" async def _cmd_clan_exit(self, bot: WechatAPIClient, sender: str, roomid: str) -> Tuple[bool, str]: player = self._get_player_with_cache(sender, roomid or "") if not player: await self._send_text_with_status(bot, sender, roomid, "道友尚未踏入仙途,请先发送:注册修仙 道号", 90) return False, "未注册" player["clan_id"] = None if self.xdb: try: self.xdb.update_player_fields(sender, player.get("group_id", ""), {"clan_id": None}) self.redis_db.invalidate_player(sender, player.get("group_id", "")) except Exception: pass self._save_player(player) self._rate_set(sender, roomid or "", "退出门派") await self._send_text_with_status(bot, sender, roomid, "✅ 已离出山门", 90) return True, "退出门派"