before adapt to web

This commit is contained in:
bridge
2025-11-20 01:02:31 +08:00
parent c5e2c2ff6d
commit 32cb34c173
4 changed files with 185 additions and 101 deletions

View File

@@ -640,14 +640,11 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin):
# 思考与目标
if self.thinking:
from src.utils.text_wrap import wrap_text
add_section(lines, "思考", wrap_text(self.thinking, 28))
add_section(lines, "思考", [self.thinking])
if self.long_term_objective:
from src.utils.text_wrap import wrap_text
add_section(lines, "长期目标", wrap_text(self.long_term_objective.content, 28))
add_section(lines, "长期目标", [self.long_term_objective.content])
if self.short_term_objective:
from src.utils.text_wrap import wrap_text
add_section(lines, "短期目标", wrap_text(self.short_term_objective, 28))
add_section(lines, "短期目标", [self.short_term_objective])
# 兵器(必有,使用颜色标记等级)
if self.weapon is not None:

View File

@@ -1,39 +1,8 @@
from typing import List, Optional, Tuple, Dict
from src.utils.text_wrap import wrap_text_by_pixels
from .rendering import map_pixel_size
def _wrap_text_by_pixels(font, text: str, max_width_px: int) -> List[str]:
"""
按像素宽度对单行文本进行硬换行,适配中英文混排(逐字符测量)。
"""
if not text:
return [""]
# 先处理显式换行符 \n避免被当作乱码字符渲染
logical_lines = str(text).split('\n')
result: List[str] = []
# 对每个逻辑行分别进行像素宽度换行
for logical_line in logical_lines:
if not logical_line:
result.append("")
continue
current = ""
for ch in logical_line:
test = current + ch
w, _ = font.size(test)
if w <= max_width_px:
current = test
else:
if current:
result.append(current)
current = ch
if current:
result.append(current)
return result if result else [""]
def draw_sidebar(
pygame_mod,
screen,
@@ -86,7 +55,8 @@ def draw_sidebar(
# 描述文字(自动换行)
usable_width = phenomenon_width
desc_lines = _wrap_text_by_pixels(font, current_phenomenon.desc, usable_width)
# 使用统一的 wrap_text_by_pixels
desc_lines = wrap_text_by_pixels(font, current_phenomenon.desc, usable_width)
for line in desc_lines:
line_surf = font.render(line, True, colors["event_text"])
screen.blit(line_surf, (phenomenon_x, phenomenon_y))
@@ -163,7 +133,8 @@ def draw_sidebar(
# 从最新事件开始,逐条向下渲染,超出底部则停止
for event in reversed(events):
event_text = str(event)
wrapped_lines = _wrap_text_by_pixels(font, event_text, usable_width)
# 使用统一的 wrap_text_by_pixels
wrapped_lines = wrap_text_by_pixels(font, event_text, usable_width)
for line in wrapped_lines:
event_surf = font.render(line, True, colors["event_text"])
screen.blit(event_surf, (title_x, event_y))
@@ -184,5 +155,3 @@ def draw_sidebar(
__all__ = ["draw_sidebar"]

View File

@@ -1,14 +1,14 @@
import math
from typing import List, Optional, Tuple, Callable
from src.classes.avatar import Avatar
from src.utils.text_wrap import wrap_text
from src.utils.text_wrap import wrap_text_by_pixels
def wrap_lines_for_tooltip(lines: List[str], max_chars_per_line: int = 28) -> List[str]:
def wrap_lines_for_tooltip(font, lines: List[str], max_width_px: int = 320) -> List[str]:
"""
将一组 tooltip 行进行字符级换行:
将一组 tooltip 行进行像素级自动换行:
- 对形如 "前缀: 内容" 的行,仅对内容部分换行,并在续行添加两个空格缩进
- 其他行超过宽度则直接按宽度切分
- 其他行超过宽度则直接按像素宽度切分
"""
wrapped: List[str] = []
for line in lines:
@@ -17,23 +17,38 @@ def wrap_lines_for_tooltip(lines: List[str], max_chars_per_line: int = 28) -> Li
if split_idx != -1:
prefix = line[: split_idx + 2]
content = line[split_idx + 2 :]
segs = wrap_text(content, max_chars_per_line)
prefix_w, _ = font.size(prefix)
indent_str = " "
indent_w, _ = font.size(indent_str)
# 内容的第一行允许宽度 = 总宽 - 前缀宽
# 内容的后续行允许宽度 = 总宽 - 缩进宽
content_first_w = max_width_px - prefix_w
content_rest_w = max_width_px - indent_w
# 边界保护:如果前缀特别长导致第一行没空间,就强制让它换行(给一个合理的最小值)
# 或者直接让它等于后续行宽度(这会造成视觉溢出,但比死循环好)
if content_first_w < 20:
content_first_w = content_rest_w
segs = wrap_text_by_pixels(font, content, content_rest_w, first_line_max_width_px=content_first_w)
if segs:
wrapped.append(prefix + segs[0])
for seg in segs[1:]:
wrapped.append(" " + seg)
wrapped.append(indent_str + seg)
else:
# 内容为空的情况
wrapped.append(line)
continue
# 无前缀情形:必要时整行切分
if len(line) > max_chars_per_line:
wrapped.extend(wrap_text(line, max_chars_per_line))
else:
wrapped.append(line)
# 无前缀情形:直接换行
wrapped.extend(wrap_text_by_pixels(font, line, max_width_px))
return wrapped
def draw_grid(pygame_mod, screen, colors, map_obj, ts: int, m: int, top_offset: int = 0):
grid_color = colors["grid"]
for gx in range(map_obj.width + 1):
@@ -333,7 +348,7 @@ def draw_avatars_and_pick_hover(
return hovered, candidate_avatars
def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mouse_y: int, font, min_width: int = 260, top_limit: int = 0):
def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mouse_y: int, font, min_width: int = 320, top_limit: int = 0):
"""
绘制tooltip支持颜色标记格式<color:R,G,B>text</color>
"""
@@ -433,18 +448,20 @@ def _render_colored_text(pygame_mod, font, text: str, default_color) -> object:
return combined
def draw_tooltip_for_avatar(pygame_mod, screen, colors, font, avatar: Avatar, tooltip_min_width: int = 260, status_bar_height: int = 32):
# 改为从 Avatar.get_hover_info 获取信息行,避免前端重复拼接
def draw_tooltip_for_avatar(pygame_mod, screen, colors, font, avatar: Avatar, tooltip_min_width: int = 320, status_bar_height: int = 32):
# 从 Avatar.get_hover_info 获取信息行
lines = avatar.get_hover_info()
draw_tooltip(pygame_mod, screen, colors, lines, *pygame_mod.mouse.get_pos(), font, min_width=tooltip_min_width, top_limit=status_bar_height)
# 使用 wrap_lines_for_tooltip 进行像素级自动换行
wrapped_lines = wrap_lines_for_tooltip(font, lines, max_width_px=tooltip_min_width)
draw_tooltip(pygame_mod, screen, colors, wrapped_lines, *pygame_mod.mouse.get_pos(), font, min_width=tooltip_min_width, top_limit=status_bar_height)
def draw_tooltip_for_region(pygame_mod, screen, colors, font, region, mouse_x: int, mouse_y: int, tooltip_min_width: int = 260, status_bar_height: int = 32):
def draw_tooltip_for_region(pygame_mod, screen, colors, font, region, mouse_x: int, mouse_y: int, tooltip_min_width: int = 320, status_bar_height: int = 32):
if region is None:
return
# 改为调用 region.get_hover_info(),并统一用 wrap_lines_for_tooltip 进行换行
# 调用 region.get_hover_info(),并统一用 wrap_lines_for_tooltip 进行换行
lines = region.get_hover_info()
wrapped_lines = wrap_lines_for_tooltip(lines, 28)
wrapped_lines = wrap_lines_for_tooltip(font, lines, max_width_px=tooltip_min_width)
draw_tooltip(pygame_mod, screen, colors, wrapped_lines, mouse_x, mouse_y, font, min_width=tooltip_min_width, top_limit=status_bar_height)

View File

@@ -1,53 +1,154 @@
def wrap_text(text: str, max_width: int = 20) -> list[str]:
import re
from typing import Optional
def wrap_text_by_pixels(font, text: str, max_width_px: int, first_line_max_width_px: Optional[int] = None) -> list[str]:
"""
将长文本按指定宽度换行
使用像素宽度对文本进行自动换行Word Wrap
支持 <color:R,G,B>...</color> 标签,确保换行时不破坏标签结构。
Args:
text: 要换行的文本
max_width: 每行的最大字符数默认20
font: 具有 .size(text) -> (w, h) 方法的对象 (如 pygame.font.Font)
text: 输入文本
max_width_px: 标准行的最大像素宽度
first_line_max_width_px: (可选) 第一行的最大像素宽度。用于处理首行缩进或前缀的情况。
如果未提供,默认为 max_width_px。
Returns:
换行后的文本列表
list[str]: 换行后的文本列表,每行都是独立的富文本字符串
"""
if not text:
return []
normalized_text = text.replace('\\n', '\n')
paragraphs = normalized_text.split('\n')
lines = []
current_line = ""
# 按换行符分割,处理已有的换行
paragraphs = text.split('\n')
wrapped_lines = []
# 标记是否处于整个文本的第一行(用于 first_line_max_width_px
is_absolute_first_line = True
# 匹配颜色标签的正则: 开头标签,或结束标签
tag_pattern = re.compile(r'(<color:\d+,\d+,\d+>)|(</color>)')
for paragraph in paragraphs:
if not paragraph.strip():
lines.append("")
if not paragraph:
wrapped_lines.append("")
# 空行也算作一行,之后的行不再是 absolute_first_line
is_absolute_first_line = False
continue
words = paragraph.split()
for word in words:
# 如果当前行加上新词会超过限制
if len(current_line) + len(word) + 1 > max_width:
if current_line:
lines.append(current_line.strip())
current_line = word
else:
# 如果单个词就超过限制,强制换行
if len(word) > max_width:
# 长词强制切分
for i in range(0, len(word), max_width):
lines.append(word[i:i + max_width])
else:
lines.append(word)
# Tokenize logic including tags
parts = tag_pattern.split(paragraph)
raw_tokens = [p for p in parts if p]
tokens = []
for rt in raw_tokens:
if tag_pattern.match(rt):
tokens.append({'type': 'tag', 'content': rt})
else:
if current_line:
current_line += " " + word
current_word = ""
for char in rt:
if char.isascii() and not char.isspace():
current_word += char
else:
if current_word:
tokens.append({'type': 'text', 'content': current_word})
current_word = ""
tokens.append({'type': 'text', 'content': char})
if current_word:
tokens.append({'type': 'text', 'content': current_word})
# Layout
current_line_str = ""
current_width = 0
active_color_tag = None
for token in tokens:
if token['type'] == 'tag':
tag_content = token['content']
current_line_str += tag_content
if tag_content.startswith('<color'):
active_color_tag = tag_content
else:
current_line = word
# 处理段落的最后一行
if current_line:
lines.append(current_line.strip())
current_line = ""
return lines
active_color_tag = None
else:
# Text token
word = token['content']
w, _ = font.size(word)
# Determine current limit
current_limit = first_line_max_width_px if (is_absolute_first_line and first_line_max_width_px is not None) else max_width_px
if current_width + w <= current_limit:
current_line_str += word
current_width += w
else:
# Need to wrap
if active_color_tag:
current_line_str += "</color>"
if current_line_str:
wrapped_lines.append(current_line_str)
# 发生换行,下一行肯定不是第一行了
is_absolute_first_line = False
# Start new line
current_line_str = ""
current_width = 0
if active_color_tag:
current_line_str += active_color_tag
# Check limit again for the new line (which is definitely not first line)
# Note: is_absolute_first_line is already False above
line_limit = max_width_px
if w > line_limit:
# Super long word handling
temp_word = word
while True:
w_temp, _ = font.size(temp_word)
# Ensure we use the correct limit for the chunk
# If we just wrapped, we are on a new line, so use max_width_px
current_chunk_limit = max_width_px
if w_temp <= current_chunk_limit:
# Remaining part fits
current_line_str += temp_word
current_width += w_temp
break
# Find cut index
cut_idx = 1
while cut_idx <= len(temp_word):
sub = temp_word[:cut_idx]
sw, _ = font.size(sub)
if sw > current_chunk_limit:
break
cut_idx += 1
cut_idx -= 1
if cut_idx == 0: cut_idx = 1
chunk = temp_word[:cut_idx]
current_line_str += chunk
if active_color_tag:
current_line_str += "</color>"
wrapped_lines.append(current_line_str)
is_absolute_first_line = False
temp_word = temp_word[cut_idx:]
current_line_str = ""
if active_color_tag:
current_line_str += active_color_tag
current_width = 0
else:
if word.isspace():
pass
else:
current_line_str += word
current_width += w
if current_line_str:
wrapped_lines.append(current_line_str)
is_absolute_first_line = False
return wrapped_lines