refactor front

This commit is contained in:
bridge
2025-10-23 00:04:04 +08:00
parent 5048baf734
commit 5b4b1b1ff8
4 changed files with 102 additions and 107 deletions

View File

@@ -1,4 +1,5 @@
import asyncio
import random
from typing import Dict, List, Optional
from src.sim.simulator import Simulator
@@ -16,6 +17,8 @@ from .rendering import (
draw_tooltip_for_region,
draw_status_bar,
STATUS_BAR_HEIGHT,
draw_small_regions,
draw_sect_headquarters,
)
from .events_panel import draw_sidebar
@@ -82,6 +85,10 @@ class Front:
self._sidebar_filter_avatar_id: Optional[str] = None
self._sidebar_filter_open: bool = False
# 侧栏筛选选项缓存(列表)与脏标记
self._sidebar_options_cache: Optional[List[tuple[str, Optional[str]]]] = None
self._sidebar_options_dirty: bool = True
# hover 轮换状态(滚轮切换)
self._hover_anchor_pos: Optional[tuple[int, int]] = None
self._hover_candidates: List[str] = [] # avatar_id 列表(当前锚点下)
@@ -100,6 +107,8 @@ class Front:
self._last_step_ms = 0
# 步进完成后,更新插值目标
self._update_avatar_display_targets()
# 世界推进后,角色增减或名称改变的可能性上升,置脏侧栏选项
self._sidebar_options_dirty = True
async def run_async(self):
pygame = self.pygame
@@ -156,7 +165,6 @@ class Front:
STATUS_BAR_HEIGHT,
)
# 底图后叠加小区域整图2x2/3x3再绘制宗门总部避免被覆盖
from .rendering import draw_sect_headquarters, draw_small_regions
draw_small_regions(pygame, self.screen, self.world, self.region_images, self.tile_images, self.tile_size, self.margin, STATUS_BAR_HEIGHT, self.tile_originals)
draw_sect_headquarters(pygame, self.screen, self.world, self.sect_images, self.tile_size, self.margin, STATUS_BAR_HEIGHT)
hovered_region = draw_region_labels(
@@ -194,10 +202,8 @@ class Front:
aid = self._sidebar_filter_avatar_id
events_to_draw = [e for e in self.events if getattr(e, "related_avatars", None) and (aid in e.related_avatars)]
# 构造下拉选项(第一个是所有人;其余为当前世界中的角色)
options: List[tuple[str, Optional[str]]] = [("所有人", None)]
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
options.append((avatar.name, avatar_id))
# 构造下拉选项(第一个是所有人;其余为当前世界中的角色)- 带缓存
options = self._get_sidebar_options_cached()
sel_label = "所有人"
if self._sidebar_filter_avatar_id is not None:
sel_avatar = self.world.avatar_manager.avatars.get(self._sidebar_filter_avatar_id)
@@ -311,13 +317,20 @@ class Front:
self._hover_last_build_ms = self._now_ms()
def _assign_avatar_images(self):
import random
# 若在上一次分配后头像集合未发生变化,且数量相等,则跳过
if not getattr(self, "_avatar_assign_dirty", True) and len(self.avatar_images) == len(self.world.avatar_manager.avatars):
return
assigned_new = False
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
if avatar_id not in self.avatar_images:
if avatar.gender == Gender.MALE and self.male_avatars:
self.avatar_images[avatar_id] = random.choice(self.male_avatars)
elif avatar.gender == Gender.FEMALE and self.female_avatars:
self.avatar_images[avatar_id] = random.choice(self.female_avatars)
assigned_new = True
# 分配完成,标记为干净;在后续状态更新时会被置脏
if assigned_new or len(self.avatar_images) == len(self.world.avatar_manager.avatars):
self._avatar_assign_dirty = False
# --- 插值辅助 ---
def _now_ms(self) -> int:
@@ -344,6 +357,10 @@ class Front:
"start_ms": float(now),
"duration_ms": float(max(1, self.step_interval_ms)),
}
# 任何插值初始化/同步都可能意味着角色集合发生变化,置脏以便头像图像分配在下一帧检查
self._avatar_assign_dirty = True
# 角色集合变动也会影响侧栏选项
self._sidebar_options_dirty = True
def _update_avatar_display_targets(self):
now = self._now_ms()
@@ -388,6 +405,16 @@ class Front:
y = float(state["start_py"]) + (float(state["target_py"]) - float(state["start_py"])) * te
return x, y
def _get_sidebar_options_cached(self) -> List[tuple[str, Optional[str]]]:
if (not self._sidebar_options_dirty) and self._sidebar_options_cache is not None:
return self._sidebar_options_cache
options: List[tuple[str, Optional[str]]] = [("所有人", None)]
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
options.append((avatar.name, avatar_id))
self._sidebar_options_cache = options
self._sidebar_options_dirty = False
return options
__all__ = ["Front"]

View File

@@ -4,15 +4,18 @@ from pathlib import Path
from src.classes.tile import TileType
# 统一的贴图类型集合,供各加载函数复用
ALL_TILE_TYPES = [
TileType.PLAIN, TileType.WATER, TileType.SEA, TileType.MOUNTAIN,
TileType.FOREST, TileType.CITY, TileType.DESERT, TileType.RAINFOREST,
TileType.GLACIER, TileType.SNOW_MOUNTAIN, TileType.VOLCANO,
TileType.GRASSLAND, TileType.SWAMP, TileType.CAVE, TileType.RUINS, TileType.FARM
]
def load_tile_images(pygame_mod, tile_size: int) -> Dict[TileType, object]:
images: Dict[TileType, object] = {}
tile_types = [
TileType.PLAIN, TileType.WATER, TileType.SEA, TileType.MOUNTAIN,
TileType.FOREST, TileType.CITY, TileType.DESERT, TileType.RAINFOREST,
TileType.GLACIER, TileType.SNOW_MOUNTAIN, TileType.VOLCANO,
TileType.GRASSLAND, TileType.SWAMP, TileType.CAVE, TileType.RUINS, TileType.FARM
]
for tile_type in tile_types:
for tile_type in ALL_TILE_TYPES:
image_path = f"assets/tiles/{tile_type.value}.png"
if os.path.exists(image_path):
image = pygame_mod.image.load(image_path)
@@ -23,13 +26,7 @@ def load_tile_images(pygame_mod, tile_size: int) -> Dict[TileType, object]:
def load_tile_originals(pygame_mod) -> Dict[TileType, object]:
originals: Dict[TileType, object] = {}
tile_types = [
TileType.PLAIN, TileType.WATER, TileType.SEA, TileType.MOUNTAIN,
TileType.FOREST, TileType.CITY, TileType.DESERT, TileType.RAINFOREST,
TileType.GLACIER, TileType.SNOW_MOUNTAIN, TileType.VOLCANO,
TileType.GRASSLAND, TileType.SWAMP, TileType.CAVE, TileType.RUINS, TileType.FARM
]
for tile_type in tile_types:
for tile_type in ALL_TILE_TYPES:
image_path = f"assets/tiles/{tile_type.value}.png"
if os.path.exists(image_path):
originals[tile_type] = pygame_mod.image.load(image_path)
@@ -104,6 +101,7 @@ __all__ = [
"load_avatar_images",
"load_sect_images",
"load_region_images",
"ALL_TILE_TYPES",
]

View File

@@ -1,5 +1,5 @@
from typing import List, Optional, Tuple, Dict
from .rendering import STATUS_BAR_HEIGHT
from .rendering import STATUS_BAR_HEIGHT, map_pixel_size
def _wrap_text_by_pixels(font, text: str, max_width_px: int) -> List[str]:
@@ -40,7 +40,8 @@ def draw_sidebar(
filter_is_open: bool,
filter_options: List[Tuple[str, Optional[str]]],
) -> Dict[str, object]:
sidebar_x = world_map.width * tile_size + margin * 2
map_px_w, _ = map_pixel_size(type("_W", (), {"map": world_map})(), tile_size)
sidebar_x = map_px_w + margin * 2
sidebar_y = margin + STATUS_BAR_HEIGHT
sidebar_rect = pygame_mod.Rect(

View File

@@ -1,13 +1,11 @@
import math
from typing import List, Optional, Tuple, Callable
from src.classes.avatar import Avatar, Gender
from src.classes.tile import TileType
from src.classes.relation import Relation
from src.classes.root import format_root_cn
from src.classes.avatar import Avatar
from src.utils.text_wrap import wrap_text
# 顶部状态栏高度(像素)
STATUS_BAR_HEIGHT = 32
TOOLTIP_MIN_WIDTH = 260
def wrap_lines_for_tooltip(lines: List[str], max_chars_per_line: int = 28) -> List[str]:
"""
将一组 tooltip 行进行字符级换行:
@@ -300,35 +298,16 @@ def draw_avatars_and_pick_hover(
screen.blit(avatar_image, (image_x, image_y))
# 名字(置于头像下方居中)
if name_font is not None:
name_text = str(getattr(avatar, "name", ""))
if name_text:
is_highlight = bool(highlight_avatar_id and avatar.id == highlight_avatar_id)
text_color = (236, 236, 236) if is_highlight else colors["text"]
text_surf = name_font.render(name_text, True, text_color)
tx = image_x + (image_rect.width - text_surf.get_width()) // 2
ty = image_y + image_rect.height + 2
if is_highlight:
pad_x = 6
pad_y = 2
w = text_surf.get_width() + pad_x * 2
h = text_surf.get_height() + pad_y * 2
bg = pygame_mod.Surface((w, h), pygame_mod.SRCALPHA)
bg.fill((0, 0, 0, 210))
screen.blit(bg, (tx - pad_x, ty - pad_y))
# 边框
rect = pygame_mod.Rect(tx - pad_x, ty - pad_y, w, h)
pygame_mod.draw.rect(screen, colors.get("tooltip_bd", (90, 90, 90)), rect, 1, border_radius=6)
screen.blit(text_surf, (tx, ty))
else:
# 轻描边
border_color = colors.get("text_border", (24, 24, 24))
border_surf = name_font.render(name_text, True, border_color)
for dx in (-1, 1, 0, 0):
for dy in (0, 0, -1, 1):
if dx == 0 and dy == 0:
continue
screen.blit(border_surf, (tx + dx, ty + dy))
screen.blit(text_surf, (tx, ty))
_draw_avatar_name_label(
pygame_mod,
screen,
colors,
name_font,
str(getattr(avatar, "name", "")),
is_highlight=bool(highlight_avatar_id and avatar.id == highlight_avatar_id),
anchor_x=image_x + image_rect.width // 2,
anchor_y=image_y + image_rect.height + 2,
)
if image_rect.collidepoint(mouse_x - image_x, mouse_y - image_y):
dist = math.hypot(mouse_x - cx, mouse_y - cy)
candidates_with_dist.append((dist, avatar))
@@ -337,33 +316,16 @@ def draw_avatars_and_pick_hover(
pygame_mod.draw.circle(screen, colors["avatar"], (cx, cy), radius)
# 名字(置于圆形下方居中)
if name_font is not None:
name_text = str(getattr(avatar, "name", ""))
if name_text:
is_highlight = bool(highlight_avatar_id and avatar.id == highlight_avatar_id)
text_color = (236, 236, 236) if is_highlight else colors["text"]
text_surf = name_font.render(name_text, True, text_color)
tx = int(cx - text_surf.get_width() / 2)
ty = int(cy + radius + 2)
if is_highlight:
pad_x = 6
pad_y = 2
w = text_surf.get_width() + pad_x * 2
h = text_surf.get_height() + pad_y * 2
bg = pygame_mod.Surface((w, h), pygame_mod.SRCALPHA)
bg.fill((0, 0, 0, 210))
screen.blit(bg, (tx - pad_x, ty - pad_y))
rect = pygame_mod.Rect(tx - pad_x, ty - pad_y, w, h)
pygame_mod.draw.rect(screen, colors.get("tooltip_bd", (90, 90, 90)), rect, 1, border_radius=6)
screen.blit(text_surf, (tx, ty))
else:
border_color = colors.get("text_border", (24, 24, 24))
border_surf = name_font.render(name_text, True, border_color)
for dx in (-1, 1, 0, 0):
for dy in (0, 0, -1, 1):
if dx == 0 and dy == 0:
continue
screen.blit(border_surf, (tx + dx, ty + dy))
screen.blit(text_surf, (tx, ty))
_draw_avatar_name_label(
pygame_mod,
screen,
colors,
name_font,
str(getattr(avatar, "name", "")),
is_highlight=bool(highlight_avatar_id and avatar.id == highlight_avatar_id),
anchor_x=cx,
anchor_y=int(cy + radius + 2),
)
dist = math.hypot(mouse_x - cx, mouse_y - cy)
if dist <= radius:
candidates_with_dist.append((dist, avatar))
@@ -373,7 +335,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: Optional[int] = None):
def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mouse_y: int, font, min_width: Optional[int] = None, top_limit: int = 0):
padding = 6
spacing = 2
surf_lines = [font.render(t, True, colors["text"]) for t in lines]
@@ -390,7 +352,6 @@ def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mou
y = mouse_y - height - 12
# 进一步夹紧,避免位于窗口上边或左边之外
x = max(0, min(x, screen_w - width))
top_limit = 0 # 如需避免覆盖状态栏,可改为 STATUS_BAR_HEIGHT
y = max(top_limit, min(y, screen_h - height))
bg_rect = pygame_mod.Rect(x, y, width, height)
pygame_mod.draw.rect(screen, colors["tooltip_bg"], bg_rect, border_radius=6)
@@ -404,35 +365,16 @@ def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mou
def draw_tooltip_for_avatar(pygame_mod, screen, colors, font, avatar: Avatar):
# 改为从 Avatar.get_hover_info 获取信息行,避免前端重复拼接
lines = avatar.get_hover_info()
draw_tooltip(pygame_mod, screen, colors, lines, *pygame_mod.mouse.get_pos(), font, min_width=260)
draw_tooltip(pygame_mod, screen, colors, 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):
if region is None:
return
# 改为调用 region.get_hover_info()
# 改为调用 region.get_hover_info(),并统一用 wrap_lines_for_tooltip 进行换行
lines = region.get_hover_info()
# 区域描述较长时做字符级换行,策略与头像思考/目标一致28 字)
wrapped_lines: list[str] = []
for line in lines:
# 针对以“描述: ”开头的行,保留前缀并仅对内容换行
if line.startswith("描述: "):
prefix = "描述: "
content = line[len(prefix):]
segs = wrap_text(content, 28)
if segs:
wrapped_lines.append(prefix + segs[0])
for seg in segs[1:]:
wrapped_lines.append(" " + seg)
else:
wrapped_lines.append(line)
else:
if len(line) > 28:
wrapped_lines.extend(wrap_text(line, 28))
else:
wrapped_lines.append(line)
# 与头像一致设置较合理的最小宽度,避免过窄导致难以阅读
draw_tooltip(pygame_mod, screen, colors, wrapped_lines, mouse_x, mouse_y, font, min_width=260)
wrapped_lines = wrap_lines_for_tooltip(lines, 28)
draw_tooltip(pygame_mod, screen, colors, wrapped_lines, mouse_x, mouse_y, font, min_width=TOOLTIP_MIN_WIDTH, top_limit=STATUS_BAR_HEIGHT)
def draw_operation_guide(pygame_mod, screen, colors, font, margin: int, auto_step: bool):
@@ -472,6 +414,7 @@ __all__ = [
"draw_tooltip_for_region",
"draw_status_bar",
"STATUS_BAR_HEIGHT",
"map_pixel_size",
]
@@ -499,4 +442,30 @@ def draw_hover_badge(pygame_mod, screen, colors, font, center_x: int, center_y:
screen.blit(surf, (rect.x + pad_x, rect.y + pad_y))
def _draw_avatar_name_label(pygame_mod, screen, colors, font, name_text: str, *, is_highlight: bool, anchor_x: int, anchor_y: int) -> None:
if not name_text:
return
text_color = (236, 236, 236) if is_highlight else colors["text"]
text_surf = font.render(name_text, True, text_color)
tx = int(anchor_x - text_surf.get_width() / 2)
ty = int(anchor_y)
if is_highlight:
pad_x = 6
pad_y = 2
w = text_surf.get_width() + pad_x * 2
h = text_surf.get_height() + pad_y * 2
bg = pygame_mod.Surface((w, h), pygame_mod.SRCALPHA)
bg.fill((0, 0, 0, 210))
screen.blit(bg, (tx - pad_x, ty - pad_y))
rect = pygame_mod.Rect(tx - pad_x, ty - pad_y, w, h)
pygame_mod.draw.rect(screen, colors.get("tooltip_bd", (90, 90, 90)), rect, 1, border_radius=6)
screen.blit(text_surf, (tx, ty))
def map_pixel_size(world_or_map, tile_size: int) -> Tuple[int, int]:
"""
计算地图像素宽高(不含 margin 与顶部偏移)。
支持传入 world含 .map或 map 对象(含 .width/.height
"""
map_obj = getattr(world_or_map, "map", world_or_map)
return map_obj.width * tile_size, map_obj.height * tile_size