This commit is contained in:
bridge
2025-11-20 23:55:22 +08:00
parent 0713881b71
commit 5a51b6638d
10 changed files with 560 additions and 34 deletions

View File

@@ -2,11 +2,15 @@
颜色系统
统一管理游戏中各种等级、稀有度的颜色方案
"""
from __future__ import annotations
import re
from typing import Protocol
class ColorGradable(Protocol):
"""支持颜色分级的协议"""
@property
def color_rgb(self) -> tuple[int, int, int]:
"""返回RGB颜色值"""
@@ -15,35 +19,43 @@ class ColorGradable(Protocol):
# ==================== 通用颜色定义 ====================
class Color:
"""预定义的颜色常量"""
# 基础颜色
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
# 品质颜色 - 从低到高
COMMON_WHITE = (255, 255, 255) # 普通/白色
UNCOMMON_GREEN = (50, 205, 50) # 不凡/绿色
RARE_BLUE = (74, 144, 226) # 稀有/蓝色
EPIC_PURPLE = (147, 112, 219) # 史诗/紫色
LEGENDARY_GOLD = (255, 215, 0) # 传说/金色
COMMON_WHITE = (255, 255, 255) # 普通/白色
UNCOMMON_GREEN = (50, 205, 50) # 不凡/绿色
RARE_BLUE = (74, 144, 226) # 稀有/蓝色
EPIC_PURPLE = (147, 112, 219) # 史诗/紫色
LEGENDARY_GOLD = (255, 215, 0) # 传说/金色
# ==================== 辅助函数 ====================
COLOR_TAG_PATTERN = re.compile(
r"<color:(\d{1,3}),(\d{1,3}),(\d{1,3})>(.*?)</color>", re.DOTALL
)
def get_color_from_mapping(
grade_value: object,
color_mapping: dict,
default_color: tuple[int, int, int] = Color.COMMON_WHITE
default_color: tuple[int, int, int] = Color.COMMON_WHITE,
) -> tuple[int, int, int]:
"""
从映射字典中获取颜色
Args:
grade_value: 等级对象
color_mapping: 等级到颜色的映射字典
default_color: 默认颜色
Returns:
RGB颜色元组
"""
@@ -54,11 +66,11 @@ def format_colored_text(text: str, color_rgb: tuple[int, int, int]) -> str:
"""
将文本格式化为带颜色标记的字符串,供前端渲染使用
格式:<color:R,G,B>text</color>
Args:
text: 要着色的文本
color_rgb: RGB颜色元组
Returns:
带颜色标记的文本字符串
"""
@@ -66,6 +78,51 @@ def format_colored_text(text: str, color_rgb: tuple[int, int, int]) -> str:
return f"<color:{r},{g},{b}>{text}</color>"
def rgb_to_hex(color_rgb: tuple[int, int, int]) -> str:
"""RGB 整数元组转 16 进制字符串(#rrggbb"""
r, g, b = color_rgb
return f"#{r:02x}{g:02x}{b:02x}"
def split_colored_segments(text: str) -> list[dict[str, str]]:
"""
将包含 <color> 标签的文本拆分为 segments
每个 segment 结构:{"text": "...", "color": "#rrggbb"}color 可选)。
"""
segments: list[dict[str, str]] = []
last_index = 0
for match in COLOR_TAG_PATTERN.finditer(text):
start, end = match.span()
if start > last_index:
plain = text[last_index:start]
if plain:
segments.append({"text": plain})
r, g, b, content = match.groups()
color_hex = rgb_to_hex((int(r), int(g), int(b)))
segments.append({"text": content, "color": color_hex})
last_index = end
if last_index < len(text):
trailing = text[last_index:]
if trailing:
segments.append({"text": trailing})
if not segments:
segments.append({"text": text})
return segments
def serialize_hover_lines(lines: list[str]) -> list[list[dict[str, str]]]:
"""将 hover 信息行转换为 segment 列表,供前端直接渲染颜色。"""
serialized: list[list[dict[str, str]]] = []
for line in lines:
serialized.append(split_colored_segments(line or ""))
return serialized
# ==================== 颜色方案映射 ====================
# 装备等级颜色方案(普通-宝物-法宝)

View File

@@ -2,7 +2,8 @@ import sys
import os
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
import uvicorn
@@ -17,6 +18,8 @@ from src.run.create_map import create_cultivation_world_map, add_sect_headquarte
from src.sim.new_avatar import make_avatars as _new_make
from src.utils.config import CONFIG
from src.classes.sect import sects_by_id
from src.classes.color import serialize_hover_lines
from src.classes.event import Event
import random
# 全局游戏实例
@@ -48,6 +51,46 @@ class ConnectionManager:
manager = ConnectionManager()
def serialize_events_for_client(events: List[Event]) -> List[dict]:
"""将事件转换为前端可用的结构。"""
serialized: List[dict] = []
for idx, event in enumerate(events):
month_stamp = getattr(event, "month_stamp", None)
stamp_int = None
year = None
month = None
if month_stamp is not None:
try:
stamp_int = int(month_stamp)
except Exception:
stamp_int = None
try:
year = int(month_stamp.get_year())
except Exception:
year = None
try:
month_obj = month_stamp.get_month()
month = int(getattr(month_obj, "value", month_obj))
except Exception:
month = None
related_raw = getattr(event, "related_avatars", None) or []
related_ids = [str(a) for a in related_raw if a is not None]
serialized.append({
"id": getattr(event, "event_id", None) or f"{stamp_int or 'evt'}-{idx}",
"text": str(event),
"content": getattr(event, "content", ""),
"year": year,
"month": month,
"month_stamp": stamp_int,
"related_avatar_ids": related_ids,
"is_major": bool(getattr(event, "is_major", False)),
"is_story": bool(getattr(event, "is_story", False)),
})
return serialized
def init_game():
"""初始化游戏世界,逻辑复用自 src/run/run.py"""
print("正在初始化游戏世界...")
@@ -98,7 +141,7 @@ async def game_loop():
"type": "tick",
"year": int(world.month_stamp.get_year()),
"month": world.month_stamp.get_month().value,
"events": [str(e) for e in events],
"events": serialize_events_for_client(events),
# 暂时只发前 50 个角色的位置更新,减少数据量
"avatars": [
{
@@ -212,12 +255,21 @@ def get_state():
except Exception as e:
return {"step": 3, "error": str(e)}
recent_events = []
try:
event_manager = getattr(world, "event_manager", None)
if event_manager:
recent_events = serialize_events_for_client(event_manager.get_recent_events(limit=50))
except Exception:
recent_events = []
return {
"status": "ok",
"year": y,
"month": m,
"avatar_count": len(world.avatar_manager.avatars),
"avatars": av_list
"avatars": av_list,
"events": recent_events
}
except Exception as e:
@@ -285,6 +337,43 @@ async def step_world():
"events_sample": [str(e) for e in events[:5]]
}
@app.get("/api/hover")
def get_hover_info(
target_type: str = Query(alias="type"),
target_id: str = Query(alias="id")
):
world = game_instance.get("world")
if world is None:
raise HTTPException(status_code=503, detail="World not initialized")
target = None
if target_type == "avatar":
target = world.avatar_manager.avatars.get(target_id)
elif target_type == "region":
if world.map and hasattr(world.map, "regions"):
regions = world.map.regions
target = regions.get(target_id)
if target is None:
try:
target = regions.get(int(target_id))
except (ValueError, TypeError):
target = None
else:
raise HTTPException(status_code=400, detail="Unsupported target type")
if target is None:
raise HTTPException(status_code=404, detail="Target not found")
if not hasattr(target, "get_hover_info"):
raise HTTPException(status_code=422, detail="Target has no hover info")
lines = target.get_hover_info() or []
return {
"id": target_id,
"type": target_type,
"name": getattr(target, "name", target_id),
"lines": serialize_hover_lines([str(line) for line in lines]),
}
def start():
"""启动服务的入口函数"""
# 改为 8002 端口