新增炼金系统
This commit is contained in:
59
.tasks/2025-11-08_2_alchemy-system.md
Normal file
59
.tasks/2025-11-08_2_alchemy-system.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 背景
|
||||
文件名: 2025-11-08_2
|
||||
创建于: 2025-11-08_23:33:12
|
||||
创建者: ASUS
|
||||
主分支: main
|
||||
任务分支: 未创建
|
||||
Yolo模式: Off
|
||||
|
||||
# 任务描述
|
||||
阅读并理解项目当前的实现,目前基础部分已经完成;接下来需要筹备炼金系统的开发工作。
|
||||
|
||||
# 项目概览
|
||||
当前项目基于 `WPSAPI` 提供插件化架构,核心系统包括:
|
||||
- `WPSBackpackSystem`:负责物品注册、背包存取与数量维护,提供 `add_item`、`set_item_quantity` 等接口,物品定义缓存于 `_item_cache`。
|
||||
- `WPSStoreSystem`:维护商店模式注册、整点刷新系统商品、处理玩家上架与购买逻辑,通过 `purchase_item`、`sell_item` 等方法与背包交互,并以 Markdown 形式输出。
|
||||
- `WPSFortuneSystem`:按整点哈希计算运势值(范围约 -0.9999 ~ 0.9999),对外提供 `get_fortune_value` 等接口,可供其他系统复用运势加成。
|
||||
|
||||
# 分析
|
||||
## 现状理解
|
||||
- 商店系统已实现模式注册(永久/轮换)、系统刷新以及玩家出售流程,依赖背包系统进行物品数量变更。
|
||||
- 背包系统支持以物品 ID 查询定义、增减用户物品数量并自动清理数量为 0 的记录。
|
||||
- 运势系统可按用户与整点计算浮动值,可作为炼金收益或成功率的修正因子。
|
||||
|
||||
- 指令入口:`炼金` 及其别名,通过聊天命令触发;支持两种模式:
|
||||
- 积分炼金:`炼金 <积分>`,仅一个参数。
|
||||
- 物品炼金:`炼金 <材料1> <材料2> <材料3> [次数]`,默认次数 1。
|
||||
- 积分炼金:投入积分后立即扣除,再依据“阶梯式”倍率表随机返还积分;倍率阶段包括 0(爆炸)、0.5(失败)、2(成功)、5(丰厚积分)、10(巨额积分)、100(传说积分),期望需匹配 `1 + 运势值 * 系数`;系数配置键名为“炼金运势系数”,在 `ProjectConfig` 中独立管理。
|
||||
- 物品炼金:固定三件材料,每次消耗各 1 件;按注册配方的基础成功率并结合运势修正决定产出;成功产出目标物品,失败产出配方指定物品;若未注册配方则必定失败并产出普通品质 `炉灰`(由炼金系统自行注册)。
|
||||
- 配方管理:炼金插件提供注册接口,参数包括三件材料、目标产物、成功率、失败产物。文本提示统一处理,不需 per 配方描述。
|
||||
|
||||
## 待确认要点
|
||||
- “爆炸/失败/成功/丰厚积分”基础概率设定为:失败、成功各为 `p`,爆炸与丰厚各为 `p/2`;“巨额积分”概率为丰厚的一半,即 `p/4`;剩余概率归传说阶段。收益期望公式为 `0∙(p/2) + 0.5∙p + 2∙p + 5∙(p/2) + 10∙(p/4) + 100∙(1 - 3.25p) = 1`,解得 `p ≈ 0.2965`,进而可推导出各阶段概率:爆炸与丰厚约 0.1482,失败与成功约 0.2965,巨额约 0.0741,传说约 0.0361。后续需考虑运势修正如何在此基础上调整概率同时保持总概率为 1。
|
||||
- 各倍率对应的文本描述尚未提供,需要在后续阶段设计统一的消息模板。
|
||||
- 运势修正:`x = fortune_value * fortune_coeff`,随机值 `y = rand01()`,以 `clamp01(x + y)` 得到最终落点;区间按奖励从低到高的累积概率设定,如爆炸长度约 0.1482、失败累计至约 0.4447 等。运势偏移会平移落点进而改变概率,需要明确是否接受此非均一调整,或需改用其他保期望策略。
|
||||
- 运势偏移带来的概率扭曲符合预期:好运用户更易触达高倍率区间,坏运则倾向低倍率,容许期望随运势自然浮动。
|
||||
- 系统需区分积分炼金与物品炼金逻辑,可能需要持久化炼金历史或冷却(目前未提及)。
|
||||
|
||||
# 提议的解决方案
|
||||
待补充(进入规划阶段后撰写)
|
||||
|
||||
# 当前执行步骤:"2. 创建任务文件"
|
||||
|
||||
# 任务进度
|
||||
[2025-11-09_00:57:54]
|
||||
- 已修改: Plugins/WPSAlchemyGame.py
|
||||
- 更改: 新增炼金插件,包含积分与物品炼金逻辑、配方注册接口、运势修正及指令解析
|
||||
- 原因: 实施规划中定义的炼金系统核心功能
|
||||
- 阻碍因素: 暂无
|
||||
- 状态: 未确认
|
||||
[2025-11-09_01:27:21]
|
||||
- 已修改: Plugins/WPSAlchemyGame.py
|
||||
- 更改: 炼金爆炸阶段增加炉灰奖励,注册炉渣物品及配方,积分结算文案更新
|
||||
- 原因: 满足新增的炉灰/炉渣奖励机制
|
||||
- 阻碍因素: 暂无
|
||||
- 状态: 未确认
|
||||
|
||||
# 最终审查
|
||||
待补充
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"database_path": "db.db",
|
||||
"always_return_ok": true,
|
||||
"plugin_dir": "Plugins",
|
||||
"alchemy_fortune_coeff": 0.03,
|
||||
"store_hourly_count": 5,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8000,
|
||||
|
||||
367
Plugins/WPSAlchemyGame.py
Normal file
367
Plugins/WPSAlchemyGame.py
Normal file
@@ -0,0 +1,367 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Sequence, 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 .WPSAPI import WPSAPI
|
||||
from .WPSBackpackSystem import (
|
||||
BackpackItemDefinition,
|
||||
BackpackItemTier,
|
||||
WPSBackpackSystem,
|
||||
)
|
||||
from .WPSConfigSystem import WPSConfigAPI
|
||||
from .WPSFortuneSystem import WPSFortuneSystem
|
||||
|
||||
|
||||
logger: ProjectConfig = Architecture.Get(ProjectConfig)
|
||||
FORTUNE_COEFF:float = logger.FindItem("alchemy_fortune_coeff", 0.03)
|
||||
logger.SaveProperties()
|
||||
|
||||
|
||||
def clamp01(value: float) -> float:
|
||||
return max(0.0, min(1.0, value))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AlchemyRecipe:
|
||||
materials: Tuple[str, str, str]
|
||||
success_item_id: str
|
||||
fail_item_id: str
|
||||
base_success_rate: float
|
||||
|
||||
|
||||
class WPSAlchemyGame(WPSAPI):
|
||||
ASH_ITEM_ID = "alchemy_ash"
|
||||
ASH_ITEM_NAME = "炉灰"
|
||||
SLAG_ITEM_ID = "alchemy_slag"
|
||||
SLAG_ITEM_NAME = "炉渣"
|
||||
MAX_BATCH_TIMES = 9999
|
||||
|
||||
_PHASE_TABLE: List[Tuple[float, float, str, str]] = [
|
||||
(0.1481481481, 0.0, "爆炸", "💥 炼金反噬,积分化为飞灰……"),
|
||||
(0.4444444444, 0.5, "失败", "😖 炼金失败,仅保留半数积分。"),
|
||||
(0.7407407407, 2.0, "成功", "😊 炼金成功,积分翻倍!"),
|
||||
(0.8888888888, 5.0, "丰厚积分", "😁 运气加成,收获丰厚积分!"),
|
||||
(0.9629629630, 10.0, "巨额积分", "🤩 巨额积分入账,今日欧气爆棚!"),
|
||||
(1.0, 100.0, "传说积分", "🌈 传说级好运!积分暴涨一百倍!"),
|
||||
]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._recipes: Dict[Tuple[str, str, str], AlchemyRecipe] = {}
|
||||
self._fortune_coeff = FORTUNE_COEFF
|
||||
|
||||
@override
|
||||
def dependencies(self) -> List[type]:
|
||||
return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem]
|
||||
|
||||
@override
|
||||
def wake_up(self) -> None:
|
||||
logger.Log(
|
||||
"Info",
|
||||
f"{ConsoleFrontColor.GREEN}WPSAlchemyGame 插件已加载{ConsoleFrontColor.RESET}",
|
||||
)
|
||||
self.register_plugin("alchemy")
|
||||
self.register_plugin("炼金")
|
||||
self._register_alchemy_items()
|
||||
|
||||
def _register_alchemy_items(self) -> None:
|
||||
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
||||
try:
|
||||
backpack.register_item(
|
||||
self.ASH_ITEM_ID,
|
||||
self.ASH_ITEM_NAME,
|
||||
BackpackItemTier.COMMON,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.Log(
|
||||
"Warning",
|
||||
f"{ConsoleFrontColor.YELLOW}注册炉灰物品时出现问题: {exc}{ConsoleFrontColor.RESET}",
|
||||
)
|
||||
try:
|
||||
backpack.register_item(
|
||||
self.SLAG_ITEM_ID,
|
||||
self.SLAG_ITEM_NAME,
|
||||
BackpackItemTier.COMMON,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.Log(
|
||||
"Warning",
|
||||
f"{ConsoleFrontColor.YELLOW}注册炉渣物品时出现问题: {exc}{ConsoleFrontColor.RESET}",
|
||||
)
|
||||
try:
|
||||
self.register_recipe(
|
||||
(self.ASH_ITEM_ID, self.ASH_ITEM_ID, self.ASH_ITEM_ID),
|
||||
self.SLAG_ITEM_ID,
|
||||
self.ASH_ITEM_ID,
|
||||
1.0,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.Log(
|
||||
"Warning",
|
||||
f"{ConsoleFrontColor.YELLOW}注册炉渣配方时出现问题: {exc}{ConsoleFrontColor.RESET}",
|
||||
)
|
||||
|
||||
def register_recipe(
|
||||
self,
|
||||
materials: Tuple[str, str, str]|Sequence[str],
|
||||
success_item_id: str,
|
||||
fail_item_id: str,
|
||||
base_success_rate: float,
|
||||
) -> None:
|
||||
if len(materials) != 3:
|
||||
raise ValueError("炼金配方必须提供三种材料")
|
||||
sorted_materials = tuple(sorted(mat.strip() for mat in materials))
|
||||
if any(not material for material in sorted_materials):
|
||||
raise ValueError("炼金材料 ID 不能为空")
|
||||
clamped_rate = clamp01(base_success_rate)
|
||||
if clamped_rate != base_success_rate:
|
||||
raise ValueError("配方成功率必须位于 0~1 之间")
|
||||
recipe = AlchemyRecipe(
|
||||
materials=sorted_materials,
|
||||
success_item_id=success_item_id.strip(),
|
||||
fail_item_id=fail_item_id.strip() or self.ASH_ITEM_ID,
|
||||
base_success_rate=base_success_rate,
|
||||
)
|
||||
self._recipes[sorted_materials] = recipe
|
||||
logger.Log(
|
||||
"Info",
|
||||
f"{ConsoleFrontColor.CYAN}已注册炼金配方 {sorted_materials} -> {success_item_id} ({base_success_rate:.2%}){ConsoleFrontColor.RESET}",
|
||||
)
|
||||
|
||||
async def callback(
|
||||
self, message: str, chat_id: int, user_id: int
|
||||
) -> Optional[str]:
|
||||
payload = self.parse_message_after_at(message).strip()
|
||||
if not payload:
|
||||
return await self.send_markdown_message(
|
||||
self._help_message(), chat_id, user_id
|
||||
)
|
||||
tokens = [token.strip() for token in payload.split() if token.strip()]
|
||||
if not tokens:
|
||||
return await self.send_markdown_message(
|
||||
self._help_message(), chat_id, user_id
|
||||
)
|
||||
|
||||
if len(tokens) == 1 and tokens[0].isdigit():
|
||||
points = int(tokens[0])
|
||||
response = await self._handle_point_alchemy(
|
||||
chat_id, user_id, points
|
||||
)
|
||||
return await self.send_markdown_message(response, chat_id, user_id)
|
||||
|
||||
if len(tokens) >= 3:
|
||||
materials = tokens[:3]
|
||||
times = 1
|
||||
if len(tokens) >= 4:
|
||||
try:
|
||||
times = int(tokens[3])
|
||||
except ValueError:
|
||||
return await self.send_markdown_message(
|
||||
"❌ 炼金次数必须是整数", chat_id, user_id
|
||||
)
|
||||
response = await self._handle_item_alchemy(
|
||||
chat_id, user_id, materials, times
|
||||
)
|
||||
return await self.send_markdown_message(response, chat_id, user_id)
|
||||
|
||||
return await self.send_markdown_message(
|
||||
self._help_message(), chat_id, user_id
|
||||
)
|
||||
|
||||
async def _handle_point_alchemy(
|
||||
self, chat_id: int, user_id: int, points: int
|
||||
) -> str:
|
||||
if points <= 0:
|
||||
return "❌ 投入积分必须大于 0"
|
||||
config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI)
|
||||
current_points = config_api.get_user_points(chat_id, 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
|
||||
)
|
||||
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(chat_id, user_id)
|
||||
extra_line = ""
|
||||
if ash_reward > 0:
|
||||
extra_line = (
|
||||
f"- 额外获得:{self.ASH_ITEM_NAME} × `{ash_reward}`\n"
|
||||
)
|
||||
return (
|
||||
"# 🔮 炼金结算\n"
|
||||
f"- 投入积分:`{points}`\n"
|
||||
f"- 结果:{phase_text}\n"
|
||||
f"- 获得倍率:×{multiplier:.1f},返还 `+{reward}` 积分\n"
|
||||
f"{extra_line}"
|
||||
f"- 当前积分:`{final_points}`"
|
||||
)
|
||||
|
||||
def _draw_point_multiplier(
|
||||
self, fortune_value: float
|
||||
) -> Tuple[float, str, str]:
|
||||
offset = fortune_value * self._fortune_coeff
|
||||
random_value = random.random()
|
||||
landing = clamp01(offset + random_value)
|
||||
for threshold, multiplier, label, text in self._PHASE_TABLE:
|
||||
if landing <= threshold:
|
||||
return multiplier, label, text
|
||||
return self._PHASE_TABLE[-1][1:]
|
||||
|
||||
async def _handle_item_alchemy(
|
||||
self,
|
||||
chat_id: int,
|
||||
user_id: int,
|
||||
materials: Sequence[str],
|
||||
times: int,
|
||||
) -> str:
|
||||
if times <= 0:
|
||||
return "❌ 炼金次数必须大于 0"
|
||||
if times > self.MAX_BATCH_TIMES:
|
||||
return f"❌ 每次最多只能炼金 {self.MAX_BATCH_TIMES} 次"
|
||||
|
||||
resolved: List[BackpackItemDefinition] = []
|
||||
for identifier in materials:
|
||||
resolved_item = self._resolve_item(identifier)
|
||||
if resolved_item is None:
|
||||
return f"❌ 未找到材料 `{identifier}`,请确认已注册"
|
||||
resolved.append(resolved_item)
|
||||
material_ids = [item.item_id for item in resolved]
|
||||
|
||||
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
||||
for item in resolved:
|
||||
owned = self._get_user_quantity(user_id, item.item_id)
|
||||
if owned < times:
|
||||
return (
|
||||
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
|
||||
)
|
||||
|
||||
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) # type: ignore[attr-defined]
|
||||
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 "- (无物品获得)"
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
def _resolve_item(
|
||||
self, identifier: str
|
||||
) -> Optional[BackpackItemDefinition]:
|
||||
identifier_lower = identifier.strip().lower()
|
||||
cursor = get_db().conn.cursor()
|
||||
cursor.execute(
|
||||
f"""
|
||||
SELECT item_id
|
||||
FROM {WPSBackpackSystem.ITEMS_TABLE}
|
||||
WHERE lower(item_id) = ? OR lower(name) = ?
|
||||
LIMIT 1
|
||||
""",
|
||||
(identifier_lower, identifier_lower),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
item_id = row["item_id"] if row else identifier.strip()
|
||||
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
||||
try:
|
||||
return backpack._get_definition(item_id) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_user_quantity(self, user_id: int, item_id: str) -> int:
|
||||
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
|
||||
for item in backpack.get_user_items(user_id):
|
||||
if item.item_id == item_id:
|
||||
return item.quantity
|
||||
return 0
|
||||
|
||||
def _help_message(self) -> str:
|
||||
return (
|
||||
"# ⚗️ 炼金指令帮助\n"
|
||||
"- `炼金 <积分>`:投入积分尝试炼金\n"
|
||||
"- `炼金 <材料1> <材料2> <材料3> [次数]`:使用三件材料进行炼金(可选次数,默认 1)\n"
|
||||
"> 建议提前备足材料及积分,谨慎开启炼金流程。"
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["WPSAlchemyGame"]
|
||||
|
||||
Reference in New Issue
Block a user