新增水晶系统

This commit is contained in:
2025-11-11 23:48:50 +08:00
parent fe4ce37c16
commit 1fdea01d13
5 changed files with 972 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
# 背景
文件名2025-11-11_1
创建于2025-11-11_21:49:00
创建者ASUS
主分支main
任务分支:无
Yolo模式Off
# 任务描述
新增高级水晶系统依赖菜园系统与冒险系统能够复用其已注册的前置物品水晶插件需要向商店注册水晶商品提供水晶变色如消耗99炉灰并等待一小时获得黑色水晶卷轴并向炼金系统注册物品配方黑色水晶卷轴*2 + 水晶 => 黑水晶)等交互。
# 项目概览
现有插件式框架包含菜园、冒险、炼金等系统均提供接口供新插件注册物品、商店条目与炼金配方PWF核心定义了插件基类、配置与依赖管理逻辑。
# 分析
待梳理菜园系统、冒险系统及相关物品注册接口的实现细节,以确认水晶系统能够复用的资源与依赖顺序;同时需要了解商店与炼金系统的注册流程与数据模型约束。
# 提议的解决方案
- 编写新的 `WPSCrystalSystem` 插件,依赖背包、商店、炼金等核心模块,初始化时完成水晶物品、配方与以物易物挂载,并通过调度器复用待机任务。
- 使用链式炼金拆分颜色演化(炉灰→粉尘→卷轴→水晶),同时为每种颜色配置等待流程与最终融合配方,确保常量均可配置化。
- 引入专属数据库表 `crystal_records` 记录变色/兑换流程状态,结合调度器在系统重启后恢复任务。
- 提供指令入口 `水晶`,支持 `变色``兑换``列表` 等子命令,并返回 Markdown 状态。
# 当前执行步骤:"执行水晶系统实现"
# 任务进度
2025-11-11_21:49:00
- 完成水晶模型、服务与主插件代码,注册物品与配方,并接入调度器、商店及指令交互。
2025-11-11_22:30:00
- 调整指令展示与兑换匹配逻辑,补充中文名称映射;基础材料获取途径尚待设计。
# 最终审查
未完成

View File

@@ -0,0 +1,6 @@
"""Crystal system plugin exports."""
from .crystal_plugin_base import WPSCrystalSystem
__all__ = ["WPSCrystalSystem"]

View File

@@ -0,0 +1,248 @@
"""Core data models and default configuration for the crystal system."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
from Plugins.WPSAlchemyGame import WPSAlchemyGame
from Plugins.WPSBackpackSystem import BackpackItemTier
@dataclass(frozen=True)
class CrystalItemDefinition:
"""Definition of a crystal-related item."""
item_id: str
name: str
tier: BackpackItemTier
description: str
@dataclass(frozen=True)
class CrystalRecipeStage:
"""A single stage recipe that is registered into the alchemy system."""
identifier: str
materials: Tuple[str, str, str]
result_item: str
fail_item: str
base_success_rate: float
description: str = ""
@dataclass(frozen=True)
class CrystalWaitStage:
"""A stage describing a delayed transformation outside the alchemy system."""
identifier: str
consumed_items: Mapping[str, int]
produced_item: str
delay_minutes: int
message: str = ""
@dataclass(frozen=True)
class CrystalFinalFusion:
"""Final fusion recipe that converts scrolls and base crystals to coloured crystals."""
identifier: str
materials: Tuple[str, str, str]
result_item: str
fail_item: str
base_success_rate: float
description: str = ""
@dataclass(frozen=True)
class CrystalExchangeEntry:
"""Definition of a crystal barter/exchange offer."""
identifier: str
required_items: Mapping[str, int]
reward_item: str
reward_amount: int = 1
metadata: Mapping[str, str] = field(default_factory=dict)
@dataclass(frozen=True)
class CrystalColorDefinition:
"""Full definition for a colour pipeline."""
color_key: str
display_name: str
chain_stages: Sequence[CrystalRecipeStage]
wait_stage: CrystalWaitStage
final_fusion: CrystalFinalFusion
CRYSTAL_BASE_ITEM_ID = "crystal_base_core"
CRYSTAL_BASE_SCROLL_ID = "crystal_base_scroll"
CRYSTAL_TINT_POWDER_ID = "crystal_tint_powder"
CRYSTAL_RESONANCE_POWDER_ID = "crystal_resonance_powder"
def _build_black_color_definition() -> CrystalColorDefinition:
"""Construct default chain definition for black crystal."""
stage1 = CrystalRecipeStage(
identifier="black_stage_1",
materials=(
WPSAlchemyGame.ASH_ITEM_ID,
WPSAlchemyGame.ASH_ITEM_ID,
CRYSTAL_TINT_POWDER_ID,
),
result_item="crystal_black_dust_stage1",
fail_item=WPSAlchemyGame.ASH_ITEM_ID,
base_success_rate=0.9,
description="将炉灰与变色粉尘融合成初阶黑色粉尘。",
)
stage2 = CrystalRecipeStage(
identifier="black_stage_2",
materials=(
stage1.result_item,
stage1.result_item,
CRYSTAL_RESONANCE_POWDER_ID,
),
result_item="crystal_black_dust_stage2",
fail_item=WPSAlchemyGame.ASH_ITEM_ID,
base_success_rate=0.75,
description="压缩初阶粉尘并注入共鸣粉形成高阶粉尘。",
)
scroll_stage = CrystalRecipeStage(
identifier="black_scroll_alchemy",
materials=(
stage2.result_item,
stage2.result_item,
CRYSTAL_BASE_SCROLL_ID,
),
result_item="crystal_black_scroll",
fail_item=WPSAlchemyGame.ASH_ITEM_ID,
base_success_rate=0.65,
description="将高阶粉尘灌注到卷轴之上,得出黑色变色卷轴。",
)
wait_stage = CrystalWaitStage(
identifier="black_scroll_darkening",
consumed_items={
WPSAlchemyGame.ASH_ITEM_ID: 99,
"crystal_black_scroll": 1,
},
produced_item="crystal_black_scroll_charged",
delay_minutes=60,
message="黑色变色卷轴正在吸收阴焰,预计一小时后完成。",
)
final_fusion = CrystalFinalFusion(
identifier="black_crystal_fusion",
materials=(
wait_stage.produced_item,
wait_stage.produced_item,
CRYSTAL_BASE_ITEM_ID,
),
result_item="crystal_black_core",
fail_item=WPSAlchemyGame.ASH_ITEM_ID,
base_success_rate=0.7,
description="两张黑色卷轴共鸣后唤醒基础晶核,形成黑水晶。",
)
return CrystalColorDefinition(
color_key="black",
display_name="黑色水晶",
chain_stages=[stage1, stage2, scroll_stage],
wait_stage=wait_stage,
final_fusion=final_fusion,
)
DEFAULT_CRYSTAL_ITEMS: Dict[str, CrystalItemDefinition] = {
CRYSTAL_BASE_ITEM_ID: CrystalItemDefinition(
item_id=CRYSTAL_BASE_ITEM_ID,
name="未调谐晶核",
tier=BackpackItemTier.RARE,
description="天然形成的晶核,等待注入颜色与秩序。",
),
CRYSTAL_BASE_SCROLL_ID: CrystalItemDefinition(
item_id=CRYSTAL_BASE_SCROLL_ID,
name="空白变色卷轴",
tier=BackpackItemTier.RARE,
description="允许运色粉尘封印其上,作为变色的载体。",
),
CRYSTAL_TINT_POWDER_ID: CrystalItemDefinition(
item_id=CRYSTAL_TINT_POWDER_ID,
name="变色粉尘",
tier=BackpackItemTier.COMMON,
description="常见的变色媒介,可与炉灰混合产生初阶粉尘。",
),
CRYSTAL_RESONANCE_POWDER_ID: CrystalItemDefinition(
item_id=CRYSTAL_RESONANCE_POWDER_ID,
name="共鸣粉末",
tier=BackpackItemTier.RARE,
description="能触发粉尘的共鸣性,有助于稳定高阶色谱。",
),
"crystal_black_dust_stage1": CrystalItemDefinition(
item_id="crystal_black_dust_stage1",
name="黑色粉尘-初阶",
tier=BackpackItemTier.RARE,
description="微量黑色素的粉尘,需要进一步压缩。",
),
"crystal_black_dust_stage2": CrystalItemDefinition(
item_id="crystal_black_dust_stage2",
name="黑色粉尘-高阶",
tier=BackpackItemTier.EPIC,
description="高纯度黑色粉尘,可用于制作黑色卷轴。",
),
"crystal_black_scroll": CrystalItemDefinition(
item_id="crystal_black_scroll",
name="黑色变色卷轴",
tier=BackpackItemTier.EPIC,
description="封装黑色粉尘力量的卷轴,但仍需阴焰淬炼。",
),
"crystal_black_scroll_charged": CrystalItemDefinition(
item_id="crystal_black_scroll_charged",
name="淬炼黑色卷轴",
tier=BackpackItemTier.EPIC,
description="经历阴焰淬炼的黑色卷轴,适合唤醒晶核。",
),
"crystal_black_core": CrystalItemDefinition(
item_id="crystal_black_core",
name="黑水晶",
tier=BackpackItemTier.LEGENDARY,
description="浓缩黑暗能量的晶体,可在多系统中作为高级材料。",
),
}
DEFAULT_CRYSTAL_COLOR_MAP: Dict[str, CrystalColorDefinition] = {
"black": _build_black_color_definition(),
}
DEFAULT_CRYSTAL_EXCHANGE_ENTRIES: Dict[str, CrystalExchangeEntry] = {
"exchange_black_scroll": CrystalExchangeEntry(
identifier="exchange_black_scroll",
required_items={
"crystal_black_dust_stage2": 2,
"crystal_tint_powder": 5,
},
reward_item="crystal_black_scroll",
metadata={
"category": "scroll",
"display_name": "黑色变色卷轴兑换",
},
),
"exchange_black_core": CrystalExchangeEntry(
identifier="exchange_black_core",
required_items={
"crystal_black_scroll_charged": 1,
CRYSTAL_BASE_ITEM_ID: 1,
"combat_material_crystal": 2,
},
reward_item="crystal_black_core",
metadata={
"category": "crystal",
"display_name": "黑水晶兑换",
},
),
}

View File

@@ -0,0 +1,374 @@
"""Crystal system primary plugin implementation."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Dict, List, Mapping, Optional, Tuple
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.clock_scheduler import get_clock_scheduler
from PWF.CoreModules.plugin_interface import DatabaseModel
from Plugins.WPSAPI import WPSAPI
from Plugins.WPSAlchemyGame import WPSAlchemyGame
from Plugins.WPSBackpackSystem import WPSBackpackSystem
from Plugins.WPSStoreSystem import WPSStoreSystem
from .crystal_models import (
DEFAULT_CRYSTAL_COLOR_MAP,
DEFAULT_CRYSTAL_EXCHANGE_ENTRIES,
DEFAULT_CRYSTAL_ITEMS,
CrystalColorDefinition,
CrystalExchangeEntry,
CrystalItemDefinition,
)
from .crystal_service import get_crystal_db_models, get_crystal_service
logger: ProjectConfig = Architecture.Get(ProjectConfig)
class WPSCrystalSystem(WPSAPI):
"""Main crystal system plugin."""
_initialized: bool = False
def __init__(self) -> None:
super().__init__()
self._service = get_crystal_service()
self._items: Dict[str, CrystalItemDefinition] = dict(DEFAULT_CRYSTAL_ITEMS)
self._colors: Dict[str, CrystalColorDefinition] = dict(DEFAULT_CRYSTAL_COLOR_MAP)
self._exchange_entries: Dict[str, CrystalExchangeEntry] = {
key.lower(): value for key, value in DEFAULT_CRYSTAL_EXCHANGE_ENTRIES.items()
}
# ------------------------------------------------------------------ #
# Plugin lifecycle
# ------------------------------------------------------------------ #
def dependencies(self) -> List[type]:
return [
WPSAPI,
WPSBackpackSystem,
WPSStoreSystem,
WPSAlchemyGame,
]
def register_db_model(self) -> List[DatabaseModel]:
return get_crystal_db_models()
def wake_up(self) -> None:
if WPSCrystalSystem._initialized:
return
WPSCrystalSystem._initialized = True
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
store: WPSStoreSystem = Architecture.Get(WPSStoreSystem)
alchemy: WPSAlchemyGame = Architecture.Get(WPSAlchemyGame)
self._ensure_prerequisite_items(backpack)
self._service.register_items(backpack, self._items)
for color_def in self._colors.values():
self._service.register_chain_recipes(alchemy, color_def.chain_stages)
self._service.register_final_fusion(alchemy, color_def.final_fusion)
self._service.register_exchange_modes(store, self._items, self._exchange_entries)
self._recover_wait_tasks()
self.register_plugin("crystal")
self.register_plugin("水晶")
logger.Log(
"Info",
f"{ConsoleFrontColor.GREEN}WPSCrystalSystem 插件已加载,当前颜色数:{len(self._colors)}{ConsoleFrontColor.RESET}",
)
# ------------------------------------------------------------------ #
# Scheduler recovery & callbacks
# ------------------------------------------------------------------ #
def _recover_wait_tasks(self) -> None:
scheduler = get_clock_scheduler()
pending_tasks = self._service.recover_pending_wait_flows()
for task in pending_tasks:
delay_ms = task.get("delay_ms", 0)
record_id = task["record_id"]
task_id = scheduler.register_task(
plugin_module=self.__module__,
plugin_class=self.__class__.__name__,
callback_name="complete_wait_flow",
delay_ms=delay_ms,
kwargs={"record_id": record_id},
)
self._service.update_task_binding(record_id, task_id)
async def complete_wait_flow(self, record_id: int) -> None:
info = self._service.mark_wait_flow_completed(record_id)
if not info:
return
chat_id = int(info["chat_id"])
user_id = int(info["user_id"])
produced_item = info["produced_item"]
message = (
"# ⏱️ 水晶淬炼完成\n"
f"- 用户:`{user_id}`\n"
f"- 获得物品:`{produced_item}`\n"
f"- 时间:{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}"
)
self._service.update_task_binding(record_id, None)
await self.send_markdown_message(message, chat_id, user_id)
# ------------------------------------------------------------------ #
# Command handling
# ------------------------------------------------------------------ #
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_help(chat_id, user_id)
tokens = payload.split()
if not tokens:
return await self._send_help(chat_id, user_id)
action = tokens[0].lower()
if action in {"help", "帮助"}:
return await self._send_help(chat_id, user_id)
if action in {"变色", "color"}:
color_key = tokens[1] if len(tokens) > 1 else ""
return await self._handle_color_wait(chat_id, user_id, color_key)
if action in {"兑换", "exchange"}:
exchange_key = tokens[1] if len(tokens) > 1 else ""
return await self._handle_exchange(chat_id, user_id, exchange_key)
if action in {"列表", "list"}:
return await self._send_overview(chat_id, user_id)
return await self._send_help(chat_id, user_id)
# ------------------------------------------------------------------ #
# Helper methods
# ------------------------------------------------------------------ #
async def _send_help(self, chat_id: int, user_id: int) -> Optional[str]:
lines = [
"# 💎 水晶系统指令",
"- `水晶 变色 <颜色>`:启动对应颜色的淬炼流程。",
"- `水晶 兑换 <ID>`:使用物品兑换指定奖励。",
"- `水晶 列表`:查看可用颜色与兑换信息。",
]
return await self.send_markdown_message("\n".join(lines), chat_id, user_id)
async def _send_overview(self, chat_id: int, user_id: int) -> Optional[str]:
lines = ["# 📦 水晶配置一览"]
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
for color_def in self._colors.values():
wait_cost = self._format_item_requirements(
backpack, color_def.wait_stage.consumed_items
)
lines.append(
f"- {color_def.display_name}|阶段:{len(color_def.chain_stages)}|等待消耗:{wait_cost}"
)
lines.append("\n## 🔄 兑换项目")
for entry in self._exchange_entries.values():
cost = self._format_item_requirements(backpack, entry.required_items)
reward_name = self._resolve_item_name(backpack, entry.reward_item)
display_name = entry.metadata.get("display_name") or reward_name
lines.append(
f"- {display_name}{entry.identifier})|消耗:{cost}|奖励:{reward_name}"
)
return await self.send_markdown_message("\n".join(lines), chat_id, user_id)
async def _handle_color_wait(
self,
chat_id: int,
user_id: int,
color_key: str,
) -> Optional[str]:
if not color_key:
return await self.send_markdown_message("❌ 请指定颜色,例如 `水晶 变色 黑色`", chat_id, user_id)
color_def = self._resolve_color(color_key)
if not color_def:
return await self.send_markdown_message("❌ 未找到对应颜色配置。", chat_id, user_id)
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
missing = self._check_item_requirements(
backpack, user_id, color_def.wait_stage.consumed_items
)
if missing:
formatted = ", ".join(
f"{self._resolve_item_name(backpack, item)}×{amount}" for item, amount in missing
)
return await self.send_markdown_message(
f"❌ 材料不足:{formatted}", chat_id, user_id
)
# Deduct materials
self._deduct_items(backpack, user_id, color_def.wait_stage.consumed_items)
record_id, expected_end = self._service.create_wait_record(
color_def, color_def.wait_stage, user_id, chat_id
)
delay_ms = max(
int((expected_end - datetime.now(timezone.utc)).total_seconds() * 1000), 0
)
scheduler = get_clock_scheduler()
task_id = scheduler.register_task(
plugin_module=self.__module__,
plugin_class=self.__class__.__name__,
callback_name="complete_wait_flow",
delay_ms=delay_ms,
kwargs={"record_id": record_id},
)
self._service.update_task_binding(record_id, task_id)
time_str = expected_end.strftime("%Y-%m-%d %H:%M")
msg = (
"# 🔁 水晶淬炼已排程\n"
f"- 颜色:{color_def.display_name}\n"
f"- 预计完成:{time_str}\n"
"- 完成后会自动发放淬炼产物。"
)
return await self.send_markdown_message(msg, chat_id, user_id)
async def _handle_exchange(
self,
chat_id: int,
user_id: int,
exchange_key: str,
) -> Optional[str]:
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
if not exchange_key:
example = self._default_exchange_example(backpack)
return await self.send_markdown_message(
f"❌ 请提供兑换ID或名称例如 `水晶 兑换 {example}`", chat_id, user_id
)
entry = self._resolve_exchange_entry(exchange_key, backpack)
if not entry:
available = ", ".join(
entry.metadata.get("display_name", entry.identifier)
for entry in self._exchange_entries.values()
)
return await self.send_markdown_message(
f"❌ 未找到对应的兑换项目。可用选项:{available}", chat_id, user_id
)
missing = self._check_item_requirements(backpack, user_id, entry.required_items)
if missing:
formatted = ", ".join(
f"{self._resolve_item_name(backpack, item)}×{amount}" for item, amount in missing
)
return await self.send_markdown_message(f"❌ 兑换所需物品不足:{formatted}", chat_id, user_id)
self._deduct_items(backpack, user_id, entry.required_items)
backpack.add_item(user_id, entry.reward_item, entry.reward_amount)
reward_name = self._resolve_item_name(backpack, entry.reward_item)
display_name = entry.metadata.get("display_name") or reward_name
msg = (
"# 🔄 兑换成功\n"
f"- 项目:{display_name}\n"
f"- 奖励:{reward_name} × {entry.reward_amount}"
)
return await self.send_markdown_message(msg, chat_id, user_id)
def _ensure_prerequisite_items(self, backpack: WPSBackpackSystem) -> None:
"""Verify key prerequisite materials exist, log warnings otherwise."""
required_items = [
"combat_material_crystal",
"combat_material_essence",
"garden_wine_maple",
"garden_wine_sage",
]
missing = []
for item_id in required_items:
try:
backpack.add_item(0, item_id, 0)
except Exception:
missing.append(item_id)
if missing:
logger.Log(
"Warning",
f"{ConsoleFrontColor.YELLOW}水晶系统启动时发现缺少前置物品: {', '.join(missing)}{ConsoleFrontColor.RESET}",
)
def _resolve_color(self, key: str) -> Optional[CrystalColorDefinition]:
lowered = key.lower()
if lowered in self._colors:
return self._colors[lowered]
for color_def in self._colors.values():
if color_def.display_name.lower() == lowered:
return color_def
return None
def _format_item_requirements(
self, backpack: WPSBackpackSystem, items: Mapping[str, int]
) -> str:
return (
", ".join(
f"{self._resolve_item_name(backpack, item_id)}×{amount}"
for item_id, amount in items.items()
)
or ""
)
def _check_item_requirements(
self,
backpack: WPSBackpackSystem,
user_id: int,
requirements: Mapping[str, int],
) -> List[Tuple[str, int]]:
missing: List[Tuple[str, int]] = []
for item_id, amount in requirements.items():
current = backpack.add_item(user_id, item_id, 0)
if current < amount:
missing.append((item_id, amount - current))
return missing
def _deduct_items(
self,
backpack: WPSBackpackSystem,
user_id: int,
requirements: Mapping[str, int],
) -> None:
for item_id, amount in requirements.items():
current = backpack.add_item(user_id, item_id, 0)
backpack.set_item_quantity(user_id, item_id, max(current - amount, 0))
def _resolve_item_name(self, backpack: WPSBackpackSystem, item_id: str) -> str:
try:
definition = backpack._get_definition(item_id)
return definition.name
except Exception:
return item_id
def _resolve_exchange_entry(
self, key: str, backpack: WPSBackpackSystem
) -> Optional[CrystalExchangeEntry]:
lowered = key.strip().lower()
if not lowered:
return None
direct = self._exchange_entries.get(lowered)
if direct:
return direct
for entry in self._exchange_entries.values():
if entry.identifier.lower() == lowered:
return entry
display_name = entry.metadata.get("display_name")
if display_name and display_name.lower() == lowered:
return entry
if entry.reward_item.lower() == lowered:
return entry
reward_name = self._resolve_item_name(backpack, entry.reward_item)
if reward_name.lower() == lowered:
return entry
return None
def _default_exchange_example(self, backpack: WPSBackpackSystem) -> str:
for entry in self._exchange_entries.values():
display_name = entry.metadata.get("display_name")
if display_name:
return display_name
reward_name = self._resolve_item_name(backpack, entry.reward_item)
if reward_name:
return reward_name
return entry.identifier
return "exchange_id"

View File

@@ -0,0 +1,310 @@
"""Service layer for the crystal system."""
from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
from PWF.Convention.Runtime.Architecture import Architecture
from PWF.Convention.Runtime.GlobalConfig import ConsoleFrontColor, ProjectConfig
from PWF.CoreModules.database import STATUS_PENDING, Database, get_db
from PWF.CoreModules.plugin_interface import DatabaseModel
from Plugins.WPSAlchemyGame import WPSAlchemyGame
from Plugins.WPSBackpackSystem import BackpackItemTier, WPSBackpackSystem
from Plugins.WPSStoreSystem import WPSStoreSystem
from .crystal_models import (
CRYSTAL_BASE_ITEM_ID,
CrystalColorDefinition,
CrystalExchangeEntry,
CrystalFinalFusion,
CrystalItemDefinition,
CrystalRecipeStage,
CrystalWaitStage,
)
CRYSTAL_TABLE_NAME = "crystal_records"
def get_crystal_db_models() -> List[DatabaseModel]:
"""Return database models required for the crystal system."""
return [
DatabaseModel(
table_name=CRYSTAL_TABLE_NAME,
column_defs={
"record_id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"user_id": "INTEGER NOT NULL",
"chat_id": "INTEGER NOT NULL",
"flow_type": "TEXT NOT NULL",
"color_key": "TEXT",
"state": "TEXT NOT NULL",
"input_items": "TEXT NOT NULL",
"current_stage": "TEXT",
"expected_end_time": "TEXT",
"scheduled_task_id": "INTEGER",
"metadata": "TEXT",
"created_at": "TEXT NOT NULL",
"updated_at": "TEXT NOT NULL",
},
),
]
class CrystalService:
"""Encapsulates shared behaviours for the crystal system."""
def __init__(self) -> None:
self._db: Database = get_db()
self._config: ProjectConfig = Architecture.Get(ProjectConfig)
self._logger = self._config
# --------------------------------------------------------------------- #
# Registration helpers
# --------------------------------------------------------------------- #
def register_items(
self,
backpack: WPSBackpackSystem,
item_definitions: Mapping[str, CrystalItemDefinition],
) -> None:
"""Register crystal items into the backpack system."""
for definition in item_definitions.values():
try:
backpack.register_item(
definition.item_id,
definition.name,
definition.tier,
definition.description,
)
except Exception as exc: # pylint: disable=broad-except
self._logger.Log(
"Warning",
f"{ConsoleFrontColor.YELLOW}注册水晶物品 {definition.item_id} 失败: {exc}{ConsoleFrontColor.RESET}",
)
def register_exchange_modes(
self,
store: WPSStoreSystem,
item_definitions: Mapping[str, CrystalItemDefinition],
exchange_entries: Mapping[str, CrystalExchangeEntry],
) -> None:
"""Register barter-friendly items into store system (as metadata only)."""
for entry in exchange_entries.values():
reward_def = item_definitions.get(entry.reward_item)
if reward_def is None:
continue
metadata = {"exchange": "true", **entry.metadata}
metadata["required_items"] = json.dumps(entry.required_items)
try:
store.register_mode(
item_id=entry.reward_item,
price=1,
limit_amount=entry.reward_amount,
metadata=metadata,
)
except Exception:
# Store可能会因模式重复而抛异常这里忽略。
continue
def register_chain_recipes(
self,
alchemy: WPSAlchemyGame,
stages: Sequence[CrystalRecipeStage],
) -> None:
"""Register crystal chain recipes into alchemy system."""
for stage in stages:
try:
alchemy.register_recipe(
stage.materials,
stage.result_item,
stage.fail_item,
stage.base_success_rate,
)
except Exception as exc: # pylint: disable=broad-except
self._logger.Log(
"Warning",
f"{ConsoleFrontColor.YELLOW}注册水晶配方 {stage.identifier} 失败: {exc}{ConsoleFrontColor.RESET}",
)
def register_final_fusion(
self,
alchemy: WPSAlchemyGame,
fusion: CrystalFinalFusion,
) -> None:
"""Register the final crystal fusion recipe."""
try:
alchemy.register_recipe(
fusion.materials,
fusion.result_item,
fusion.fail_item,
fusion.base_success_rate,
)
except Exception as exc: # pylint: disable=broad-except
self._logger.Log(
"Warning",
f"{ConsoleFrontColor.YELLOW}注册终端水晶配方 {fusion.identifier} 失败: {exc}{ConsoleFrontColor.RESET}",
)
# --------------------------------------------------------------------- #
# Wait-flow handling
# --------------------------------------------------------------------- #
def create_wait_record(
self,
color_def: CrystalColorDefinition,
wait_stage: CrystalWaitStage,
user_id: int,
chat_id: int,
) -> Tuple[int, datetime]:
"""Create a wait-flow record and enqueue scheduler task."""
now = datetime.now(timezone.utc)
expected_end = now + timedelta(minutes=wait_stage.delay_minutes)
payload = {
"color_key": color_def.color_key,
"wait_stage": wait_stage.identifier,
"produced_item": wait_stage.produced_item,
}
cursor = self._db.conn.cursor()
cursor.execute(
f"""
INSERT INTO {CRYSTAL_TABLE_NAME} (
user_id, chat_id, flow_type, color_key, state, input_items,
current_stage, expected_end_time, metadata, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
user_id,
chat_id,
"wait_flow",
color_def.color_key,
STATUS_PENDING,
json.dumps(wait_stage.consumed_items),
wait_stage.identifier,
expected_end.isoformat(),
json.dumps(payload),
now.isoformat(),
now.isoformat(),
),
)
record_id = cursor.lastrowid
self._db.conn.commit()
return int(record_id), expected_end
def update_task_binding(self, record_id: int, task_id: Optional[int]) -> None:
"""Update the scheduled task binding for a record."""
cursor = self._db.conn.cursor()
cursor.execute(
f"UPDATE {CRYSTAL_TABLE_NAME} SET scheduled_task_id = ? WHERE record_id = ?",
(task_id, record_id),
)
self._db.conn.commit()
def mark_wait_flow_completed(self, record_id: int) -> Optional[Dict[str, str]]:
"""Scheduler callback when a wait flow is ready."""
cursor = self._db.conn.cursor()
cursor.execute(
f"""
SELECT record_id, user_id, chat_id, metadata
FROM {CRYSTAL_TABLE_NAME}
WHERE record_id = ? AND flow_type = 'wait_flow'
""",
(record_id,),
)
row = cursor.fetchone()
if not row:
return None
metadata = json.loads(row["metadata"]) if row["metadata"] else {}
produced_item = metadata.get("produced_item")
user_id = int(row["user_id"])
chat_id = int(row["chat_id"])
payload = {
"state": "completed",
"updated_at": datetime.now(timezone.utc).isoformat(),
}
cursor.execute(
f"""
UPDATE {CRYSTAL_TABLE_NAME}
SET state = :state,
updated_at = :updated_at
WHERE record_id = :record_id
""",
{"state": "completed", "updated_at": payload["updated_at"], "record_id": record_id},
)
self._db.conn.commit()
if not produced_item:
return None
backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem)
try:
backpack.add_item(user_id, produced_item, 1)
except Exception as exc: # pylint: disable=broad-except
self._logger.Log(
"Error",
f"{ConsoleFrontColor.RED}发放等待产物 {produced_item} 失败: {exc}{ConsoleFrontColor.RESET}",
)
return None
return {
"produced_item": produced_item,
"user_id": str(user_id),
"chat_id": str(chat_id),
}
# --------------------------------------------------------------------- #
# Recovery
# --------------------------------------------------------------------- #
def recover_pending_wait_flows(self) -> List[Dict[str, int]]:
"""Recover pending wait-flow tasks when the plugin is reloaded."""
cursor = self._db.conn.cursor()
cursor.execute(
f"""
SELECT record_id, current_stage, expected_end_time, scheduled_task_id
FROM {CRYSTAL_TABLE_NAME}
WHERE flow_type = 'wait_flow' AND state = ?
""",
(STATUS_PENDING,),
)
rows = cursor.fetchall()
now = datetime.now(timezone.utc)
tasks: List[Dict[str, int]] = []
for row in rows:
record_id = int(row["record_id"])
expected_end = row["expected_end_time"]
scheduled_task_id = row["scheduled_task_id"]
delay_ms = 0
if expected_end:
try:
target_dt = datetime.fromisoformat(expected_end)
remaining = target_dt - now
delay_ms = max(int(remaining.total_seconds() * 1000), 0)
except ValueError:
delay_ms = 0
if scheduled_task_id:
continue
tasks.append({"record_id": record_id, "delay_ms": delay_ms})
return tasks
_CRYSTAL_SERVICE: Optional[CrystalService] = None
def get_crystal_service() -> CrystalService:
"""Return singleton crystal service instance."""
global _CRYSTAL_SERVICE
if _CRYSTAL_SERVICE is None:
_CRYSTAL_SERVICE = CrystalService()
return _CRYSTAL_SERVICE