diff --git a/Plugins/WPSAlchemyGame.py b/Plugins/WPSAlchemyGame.py index 04c1c7c..71f24a2 100644 --- a/Plugins/WPSAlchemyGame.py +++ b/Plugins/WPSAlchemyGame.py @@ -1,13 +1,16 @@ from __future__ import annotations +import json import random from collections import defaultdict, Counter from dataclasses import dataclass +from datetime import datetime, timedelta from typing import Dict, List, Optional, Sequence, Set, Tuple, override from PWF.Convention.Runtime.Architecture import Architecture from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig -from PWF.CoreModules.database import get_db +from PWF.CoreModules.database import get_db, STATUS_COMPLETED +from PWF.CoreModules.plugin_interface import DatabaseModel from .WPSAPI import WPSAPI from .WPSBackpackSystem import ( @@ -60,11 +63,38 @@ class WPSAlchemyGame(WPSAPI): self._success_index: Dict[str, Set[Tuple[str, str, str]]] = defaultdict(set) self._fail_index: Dict[str, Set[Tuple[str, str, str]]] = defaultdict(set) self._fortune_coeff = FORTUNE_COEFF + # 从配置读取冷却时间(分钟) + from PWF.CoreModules.flags import get_internal_debug + cooldown_minutes = logger.FindItem("alchemy_cooldown_minutes", 2) + if get_internal_debug(): + cooldown_minutes = 0 + self._cooldown_minutes = cooldown_minutes + self._cooldown_ms = int(cooldown_minutes * 60 * 1000) + logger.SaveProperties() @override def dependencies(self) -> List[type]: return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem, WPSStoreSystem] + @override + def register_db_model(self) -> DatabaseModel: + """注册炼金记录数据库表""" + return DatabaseModel( + table_name="alchemy_records", + column_defs={ + "alchemy_id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "user_id": "INTEGER NOT NULL", + "chat_id": "INTEGER NOT NULL", + "alchemy_type": "TEXT NOT NULL", + "input_data": "TEXT NOT NULL", + "start_time": "TEXT NOT NULL", + "expected_end_time": "TEXT NOT NULL", + "status": "TEXT NOT NULL", + "result_data": "TEXT", + "scheduled_task_id": "INTEGER", + }, + ) + @override def wake_up(self) -> None: logger.Log( @@ -74,6 +104,8 @@ class WPSAlchemyGame(WPSAPI): self.register_plugin("alchemy") self.register_plugin("炼金") self._register_alchemy_items() + # 恢复过期炼金 + self.recover_overdue_alchemy() def _register_alchemy_items(self) -> None: backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) @@ -231,6 +263,11 @@ class WPSAlchemyGame(WPSAPI): self._help_message(), chat_id, user_id ) + # 处理状态查询命令 + if len(tokens) == 1 and tokens[0] in ["状态", "status"]: + response = await self._handle_status_query(chat_id, user_id) + return await self.send_markdown_message(response, chat_id, user_id) + if len(tokens) == 1 and tokens[0].isdigit(): points = int(tokens[0]) response = await self._handle_point_alchemy( @@ -262,45 +299,73 @@ class WPSAlchemyGame(WPSAPI): ) -> str: if points <= 0: return "❌ 投入积分必须大于 0" + + # 检查冷却 + is_on_cooldown, cooldown_msg = self._check_cooldown(user_id) + if is_on_cooldown: + return cooldown_msg + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) current_points = config_api.get_user_points(user_id) if current_points < points: return f"❌ 积分不足,需要 {points} 分,当前仅有 {current_points} 分" + # 扣除积分 await config_api.adjust_user_points( chat_id, user_id, -points, "炼金消耗" ) - fortune_system: WPSFortuneSystem = Architecture.Get(WPSFortuneSystem) - fortune_value = fortune_system.get_fortune_value(user_id) - multiplier, phase_label, phase_text = self._draw_point_multiplier( - fortune_value + # 创建炼金记录 + input_data = {"type": "point", "points": points} + alchemy_id = self._create_alchemy_record( + user_id, chat_id, "point", input_data ) - reward = int(points * multiplier) - if reward: - await config_api.adjust_user_points( - chat_id, user_id, reward, f"炼金收益({phase_label})" - ) - ash_reward = 0 - if multiplier == 0.0: - ash_reward = min(points // 10, 99) - if ash_reward > 0: - backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) - backpack.add_item(user_id, self.ASH_ITEM_ID, ash_reward) - final_points = config_api.get_user_points(user_id) - extra_line = "" - if ash_reward > 0: - extra_line = ( - f"- 额外获得:{self.ASH_ITEM_NAME} × `{ash_reward}`\n" + # 注册定时任务 + task_id = None + if self._cooldown_ms > 0: + task_id = self.register_clock( + self._settle_alchemy_callback, + self._cooldown_ms, + kwargs={ + "alchemy_id": alchemy_id, + "user_id": user_id, + "chat_id": chat_id, + }, ) + # 更新记录的任务ID + cursor = get_db().conn.cursor() + cursor.execute( + "UPDATE alchemy_records SET scheduled_task_id = ? WHERE alchemy_id = ?", + (task_id, alchemy_id), + ) + get_db().conn.commit() + else: + # Debug模式,立即结算 + success, msg, rewards = self.settle_alchemy(alchemy_id) + return msg + + # 计算预计完成时间 + cursor = get_db().conn.cursor() + cursor.execute( + "SELECT expected_end_time FROM alchemy_records WHERE alchemy_id = ?", + (alchemy_id,), + ) + record = cursor.fetchone() + expected_end_time = datetime.fromisoformat(record["expected_end_time"]) + + from PWF.CoreModules.flags import get_internal_debug + debug_hint = " **[DEBUG模式]**" if get_internal_debug() else "" + + time_str = "立即结算" if self._cooldown_minutes == 0 else f"{self._cooldown_minutes} 分钟" + return ( - "# 🔮 炼金结算\n" + f"# ⚗️ 炼金开始{debug_hint}\n" + f"- 类型:积分炼金\n" f"- 投入积分:`{points}`\n" - f"- 结果:{phase_text}\n" - f"- 获得倍率:×{multiplier:.1f},返还 `+{reward}` 积分\n" - f"{extra_line}" - f"- 当前积分:`{final_points}`" + f"- 预计耗时:{time_str}\n" + f"- 预计完成:{expected_end_time.strftime('%Y-%m-%d %H:%M')}\n" + f"- 状态:炼金进行中..." ) def _draw_point_multiplier( @@ -326,6 +391,11 @@ class WPSAlchemyGame(WPSAPI): if times > self.MAX_BATCH_TIMES: return f"❌ 每次最多只能炼金 {self.MAX_BATCH_TIMES} 次" + # 检查冷却 + is_on_cooldown, cooldown_msg = self._check_cooldown(user_id) + if is_on_cooldown: + return cooldown_msg + resolved: List[BackpackItemDefinition] = [] for identifier in materials: resolved_item = self._resolve_item(identifier) @@ -342,70 +412,69 @@ class WPSAlchemyGame(WPSAPI): f"❌ 材料 `{item.name}` 数量不足,需要 {times} 个,当前仅有 {owned} 个" ) + # 扣除材料 for item in resolved: current = self._get_user_quantity(user_id, item.item_id) backpack.set_item_quantity( user_id, item.item_id, current - times ) - recipe = self._recipes.get(tuple(sorted(material_ids))) - fortune_system: WPSFortuneSystem = Architecture.Get(WPSFortuneSystem) - fortune_value = fortune_system.get_fortune_value(user_id) - adjusted_rate = ( - clamp01(recipe.base_success_rate + fortune_value * self._fortune_coeff) - if recipe - else 0.0 + # 创建炼金记录 + input_data = { + "type": "item", + "materials": material_ids, + "times": times, + } + alchemy_id = self._create_alchemy_record( + user_id, chat_id, "item", input_data ) - success_count = 0 - fail_count = 0 - rewards: Dict[str, int] = {} - for _ in range(times): - if recipe and random.random() < adjusted_rate: - reward_id = recipe.success_item_id - success_count += 1 - else: - reward_id = ( - recipe.fail_item_id if recipe else self.ASH_ITEM_ID - ) - fail_count += 1 - backpack.add_item(user_id, reward_id, 1) - rewards[reward_id] = rewards.get(reward_id, 0) + 1 + # 注册定时任务 + task_id = None + if self._cooldown_ms > 0: + task_id = self.register_clock( + self._settle_alchemy_callback, + self._cooldown_ms, + kwargs={ + "alchemy_id": alchemy_id, + "user_id": user_id, + "chat_id": chat_id, + }, + ) + # 更新记录的任务ID + cursor = get_db().conn.cursor() + cursor.execute( + "UPDATE alchemy_records SET scheduled_task_id = ? WHERE alchemy_id = ?", + (task_id, alchemy_id), + ) + get_db().conn.commit() + else: + # Debug模式,立即结算 + success, msg, rewards = self.settle_alchemy(alchemy_id) + return msg - details = [] - for item_id, count in rewards.items(): - try: - definition = backpack._get_definition(item_id) - item_name = definition.name - except Exception: - item_name = item_id - details.append(f"- {item_name} × **{count}**") - - success_line = ( - f"- 成功次数:`{success_count}`" - if recipe - else "- 成功次数:`0`(未知配方必定失败)" + # 计算预计完成时间 + cursor = get_db().conn.cursor() + cursor.execute( + "SELECT expected_end_time FROM alchemy_records WHERE alchemy_id = ?", + (alchemy_id,), ) - fail_line = ( - f"- 失败次数:`{fail_count}`" - if recipe - else f"- 失败次数:`{times}`" - ) - rate_line = ( - f"- 基础成功率:`{recipe.base_success_rate:.2%}`" - if recipe - else "- ✅ 未知配方仅产出炉灰" - ) - rewards_block = "\n".join(details) if details else "- (无物品获得)" - + record = cursor.fetchone() + expected_end_time = datetime.fromisoformat(record["expected_end_time"]) + + from PWF.CoreModules.flags import get_internal_debug + debug_hint = " **[DEBUG模式]**" if get_internal_debug() else "" + + time_str = "立即结算" if self._cooldown_minutes == 0 else f"{self._cooldown_minutes} 分钟" + material_names = "、".join([item.name for item in resolved]) + return ( - "# ⚗️ 物品炼金结果\n" - f"- 投入材料:{'、'.join([item.name for item in resolved])} × {times}\n" - f"{success_line}\n" - f"{fail_line}\n" - f"{rate_line}\n" - "- 获得物品:\n" - f"{rewards_block}" + f"# ⚗️ 炼金开始{debug_hint}\n" + f"- 类型:物品炼金\n" + f"- 投入材料:{material_names} × {times}\n" + f"- 预计耗时:{time_str}\n" + f"- 预计完成:{expected_end_time.strftime('%Y-%m-%d %H:%M')}\n" + f"- 状态:炼金进行中..." ) def _resolve_item( @@ -437,12 +506,356 @@ class WPSAlchemyGame(WPSAPI): return item.quantity return 0 + def _check_cooldown(self, user_id: int) -> Tuple[bool, Optional[str]]: + """检查用户是否在冷却中""" + cursor = get_db().conn.cursor() + cursor.execute( + """ + SELECT alchemy_id, expected_end_time, alchemy_type + FROM alchemy_records + WHERE user_id = ? AND status = 'in_progress' + ORDER BY start_time DESC + LIMIT 1 + """, + (user_id,), + ) + record = cursor.fetchone() + + if not record: + return False, None + + expected_end = datetime.fromisoformat(record["expected_end_time"]) + now = datetime.now() + + if now >= expected_end: + # 已过期,自动结算 + try: + self.settle_alchemy(record["alchemy_id"]) + except Exception as e: + logger.Log( + "Error", + f"{ConsoleFrontColor.RED}自动结算过期炼金失败: {e}{ConsoleFrontColor.RESET}", + ) + return False, None + + # 仍在冷却中 + remaining = expected_end - now + remaining_minutes = int(remaining.total_seconds() / 60) + 1 + alchemy_type_name = "积分炼金" if record["alchemy_type"] == "point" else "物品炼金" + + return True, ( + f"❌ 炼金冷却中\n" + f"- 上次炼金类型:{alchemy_type_name}\n" + f"- 预计完成:{expected_end.strftime('%Y-%m-%d %H:%M')}\n" + f"- 剩余时间:约 {remaining_minutes} 分钟\n" + f"- 请等待冷却结束后再试" + ) + + def _create_alchemy_record( + self, user_id: int, chat_id: int, alchemy_type: str, input_data: Dict + ) -> int: + """创建炼金记录""" + start_time = datetime.now() + expected_end_time = start_time + timedelta(minutes=self._cooldown_minutes) + + cursor = get_db().conn.cursor() + cursor.execute( + """ + INSERT INTO alchemy_records + (user_id, chat_id, alchemy_type, input_data, start_time, expected_end_time, status) + VALUES (?, ?, ?, ?, ?, ?, 'in_progress') + """, + ( + user_id, + chat_id, + alchemy_type, + json.dumps(input_data), + start_time.isoformat(), + expected_end_time.isoformat(), + ), + ) + alchemy_id = cursor.lastrowid + get_db().conn.commit() + return alchemy_id + + def settle_alchemy(self, alchemy_id: int) -> Tuple[bool, str, Optional[Dict]]: + """结算炼金""" + cursor = get_db().conn.cursor() + cursor.execute( + "SELECT * FROM alchemy_records WHERE alchemy_id = ?", + (alchemy_id,), + ) + record = cursor.fetchone() + + if not record: + return False, "❌ 炼金记录不存在", None + + if record["status"] != "in_progress": + return False, f"❌ 炼金已结算(状态:{record['status']})", None + + user_id = record["user_id"] + chat_id = record["chat_id"] + alchemy_type = record["alchemy_type"] + input_data = json.loads(record["input_data"]) + + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + fortune_system: WPSFortuneSystem = Architecture.Get(WPSFortuneSystem) + fortune_value = fortune_system.get_fortune_value(user_id) + + result_data: Dict = {} + message_lines = ["# 🔮 炼金结算\n"] + + try: + if alchemy_type == "point": + # 积分炼金结算 + points = input_data["points"] + multiplier, phase_label, phase_text = self._draw_point_multiplier( + fortune_value + ) + reward = int(points * multiplier) + + if reward: + config_api.adjust_user_points_sync( + user_id, reward, f"炼金收益({phase_label})" + ) + + ash_reward = 0 + if multiplier == 0.0: + ash_reward = min(points // 10, 99) + if ash_reward > 0: + backpack.add_item(user_id, self.ASH_ITEM_ID, ash_reward) + + final_points = config_api.get_user_points(user_id) + extra_line = "" + if ash_reward > 0: + extra_line = f"- 额外获得:{self.ASH_ITEM_NAME} × `{ash_reward}`\n" + + message_lines.extend([ + f"- 投入积分:`{points}`\n", + f"- 结果:{phase_text}\n", + f"- 获得倍率:×{multiplier:.1f},返还 `+{reward}` 积分\n", + extra_line, + f"- 当前积分:`{final_points}`", + ]) + + result_data = { + "multiplier": multiplier, + "reward_points": reward, + "ash_reward": ash_reward, + } + + elif alchemy_type == "item": + # 物品炼金结算 + materials = input_data["materials"] + times = input_data["times"] + + # 解析材料 + resolved: List[BackpackItemDefinition] = [] + for material_id in materials: + try: + definition = backpack._get_definition(material_id) + resolved.append(definition) + except Exception: + # 如果材料不存在,使用ID + pass + + recipe = self._recipes.get(tuple(sorted(materials))) + adjusted_rate = ( + clamp01(recipe.base_success_rate + fortune_value * self._fortune_coeff) + if recipe + else 0.0 + ) + + success_count = 0 + fail_count = 0 + rewards: Dict[str, int] = {} + + for _ in range(times): + if recipe and random.random() < adjusted_rate: + reward_id = recipe.success_item_id + success_count += 1 + else: + reward_id = ( + recipe.fail_item_id if recipe else self.ASH_ITEM_ID + ) + fail_count += 1 + backpack.add_item(user_id, reward_id, 1) + rewards[reward_id] = rewards.get(reward_id, 0) + 1 + + details = [] + for item_id, count in rewards.items(): + try: + definition = backpack._get_definition(item_id) + item_name = definition.name + except Exception: + item_name = item_id + details.append(f"- {item_name} × **{count}**") + + success_line = ( + f"- 成功次数:`{success_count}`" + if recipe + else "- 成功次数:`0`(未知配方必定失败)" + ) + fail_line = ( + f"- 失败次数:`{fail_count}`" + if recipe + else f"- 失败次数:`{times}`" + ) + rate_line = ( + f"- 基础成功率:`{recipe.base_success_rate:.2%}`" + if recipe + else "- ✅ 未知配方仅产出炉灰" + ) + rewards_block = "\n".join(details) if details else "- (无物品获得)" + + material_names = "、".join([item.name for item in resolved]) + message_lines.extend([ + f"- 投入材料:{material_names} × {times}\n", + f"{success_line}\n", + f"{fail_line}\n", + f"{rate_line}\n", + "- 获得物品:\n", + f"{rewards_block}", + ]) + + result_data = { + "success_count": success_count, + "fail_count": fail_count, + "rewards": rewards, + } + else: + return False, f"❌ 未知的炼金类型:{alchemy_type}", None + + # 更新记录状态 + cursor.execute( + """ + UPDATE alchemy_records + SET status = 'completed', result_data = ? + WHERE alchemy_id = ? + """, + (json.dumps(result_data), alchemy_id), + ) + + # 更新定时任务状态 + scheduled_task_id = record.get("scheduled_task_id") + if scheduled_task_id: + try: + get_db().update_task_status(int(scheduled_task_id), STATUS_COMPLETED) + except Exception: + pass + + get_db().conn.commit() + + return True, "".join(message_lines), result_data + + except Exception as e: + logger.Log( + "Error", + f"{ConsoleFrontColor.RED}结算炼金失败: {e}{ConsoleFrontColor.RESET}", + ) + # 标记为失败 + cursor.execute( + "UPDATE alchemy_records SET status = 'failed' WHERE alchemy_id = ?", + (alchemy_id,), + ) + get_db().conn.commit() + return False, f"❌ 结算失败:{str(e)}", None + + async def _settle_alchemy_callback( + self, alchemy_id: int, user_id: int, chat_id: int + ) -> None: + """炼金结算回调(时钟任务)""" + success, msg, rewards = self.settle_alchemy(alchemy_id) + await self.send_markdown_message(msg, chat_id, user_id) + + def _get_user_alchemy_status(self, user_id: int) -> Optional[Dict]: + """获取用户当前炼金状态""" + cursor = get_db().conn.cursor() + cursor.execute( + """ + SELECT * FROM alchemy_records + WHERE user_id = ? AND status = 'in_progress' + ORDER BY start_time DESC + LIMIT 1 + """, + (user_id,), + ) + record = cursor.fetchone() + return dict(record) if record else None + + async def _handle_status_query(self, chat_id: int, user_id: int) -> str: + """处理状态查询""" + record = self._get_user_alchemy_status(user_id) + + if not record: + return ( + "# ⚗️ 炼金状态\n" + "- 状态:无进行中的炼金\n" + "- 可以开始新的炼金" + ) + + alchemy_type = record["alchemy_type"] + alchemy_type_name = "积分炼金" if alchemy_type == "point" else "物品炼金" + start_time = datetime.fromisoformat(record["start_time"]) + expected_end_time = datetime.fromisoformat(record["expected_end_time"]) + now = datetime.now() + + if now >= expected_end_time: + remaining_str = "已完成,等待结算" + else: + remaining = expected_end_time - now + remaining_minutes = int(remaining.total_seconds() / 60) + 1 + remaining_str = f"约 {remaining_minutes} 分钟" + + return ( + "# ⚗️ 炼金状态\n" + f"- 状态:进行中\n" + f"- 类型:{alchemy_type_name}\n" + f"- 开始时间:{start_time.strftime('%Y-%m-%d %H:%M')}\n" + f"- 预计完成:{expected_end_time.strftime('%Y-%m-%d %H:%M')}\n" + f"- 剩余时间:{remaining_str}" + ) + + def recover_overdue_alchemy(self) -> None: + """恢复过期但未结算的炼金""" + cursor = get_db().conn.cursor() + cursor.execute( + """ + SELECT alchemy_id FROM alchemy_records + WHERE status = 'in_progress' AND expected_end_time < ? + """, + (datetime.now().isoformat(),), + ) + + overdue_records = cursor.fetchall() + + for record in overdue_records: + logger.Log( + "Warning", + f"{ConsoleFrontColor.YELLOW}发现过期炼金 {record['alchemy_id']},执行恢复结算{ConsoleFrontColor.RESET}", + ) + try: + self.settle_alchemy(record["alchemy_id"]) + except Exception as e: + logger.Log( + "Error", + f"{ConsoleFrontColor.RED}恢复炼金 {record['alchemy_id']} 失败: {e}{ConsoleFrontColor.RESET}", + ) + + if overdue_records: + logger.Log( + "Info", + f"{ConsoleFrontColor.GREEN}恢复了 {len(overdue_records)} 个过期炼金{ConsoleFrontColor.RESET}", + ) + def _help_message(self) -> str: return ( "# ⚗️ 炼金指令帮助\n" "- `炼金 <积分>`:投入积分尝试炼金\n" "- `炼金 <材料1> <材料2> <材料3> [次数]`:使用三件材料进行炼金(可选次数,默认 1)\n" - "> 建议提前备足材料及积分,谨慎开启炼金流程。" + "- `炼金 状态`:查询当前炼金状态\n" + "> 建议提前备足材料及积分,谨慎开启炼金流程。炼金需要等待一定时间后才会获得结果。" )