diff --git a/src/front/__init__.py b/src/front/__init__.py index 303af52..ad017f0 100644 --- a/src/front/__init__.py +++ b/src/front/__init__.py @@ -1,5 +1,6 @@ from .app import Front +from .layout import LayoutConfig, calculate_layout, get_fullscreen_resolution -__all__ = ["Front"] +__all__ = ["Front", "LayoutConfig", "calculate_layout", "get_fullscreen_resolution"] diff --git a/src/front/app.py b/src/front/app.py index 5db4717..910d7aa 100644 --- a/src/front/app.py +++ b/src/front/app.py @@ -17,12 +17,12 @@ from .rendering import ( draw_tooltip_for_avatar, draw_tooltip_for_region, draw_status_bar, - STATUS_BAR_HEIGHT, draw_small_regions, draw_sect_headquarters, ) from .events_panel import draw_sidebar from .menu import PauseMenu +from .layout import calculate_layout, get_fullscreen_resolution class Front: @@ -30,21 +30,15 @@ class Front: self, simulator: Simulator, *, - tile_size: int = 32, - margin: int = 8, step_interval_ms: int = 400, window_title: str = "Cultivation World Simulator", font_path: Optional[str] = None, - sidebar_width: int = 300, ): self.world = simulator.world self.simulator = simulator - self.tile_size = tile_size - self.margin = margin self.step_interval_ms = step_interval_ms self.window_title = window_title self.font_path = font_path - self.sidebar_width = sidebar_width self._last_step_ms = 0 self.events: List[Event] = [] @@ -54,31 +48,61 @@ class Front: pygame.init() pygame.font.init() - width_px = self.world.map.width * tile_size + margin * 2 + sidebar_width - height_px = self.world.map.height * tile_size + margin * 2 + STATUS_BAR_HEIGHT - self.screen = pygame.display.set_mode((width_px, height_px)) + # 获取可用屏幕分辨率(排除任务栏)并计算动态布局 + screen_width, screen_height = get_fullscreen_resolution(pygame) + self.layout = calculate_layout( + screen_width, + screen_height, + self.world.map.width, + self.world.map.height + ) + + # 使用动态布局参数 + self.tile_size = self.layout.tile_size + self.margin = self.layout.margin + self.sidebar_width = self.layout.sidebar_width + + # 创建无边框最大化窗口(底部保留任务栏空间) + self.screen = pygame.display.set_mode((self.layout.screen_width, self.layout.screen_height), pygame.NOFRAME) pygame.display.set_caption(window_title) + # 将窗口移动到屏幕左上角 (0, 0),顶部紧贴屏幕边缘 + import os + if os.name == 'nt': # Windows系统 + try: + import ctypes + hwnd = pygame.display.get_wm_info()['window'] + # SWP_NOZORDER = 0x0004, SWP_SHOWWINDOW = 0x0040 + # 设置窗口位置到 (0, 0),不改变Z顺序 + ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, + self.layout.screen_width, + self.layout.screen_height, + 0x0004) + except Exception: + pass # 如果设置失败也不影响使用 + # 设置窗口图标 icon_path = "assets/icon.png" if os.path.exists(icon_path): icon = pygame.image.load(icon_path) pygame.display.set_icon(icon) - self.font = create_font(self.pygame, 16, self.font_path) - self.tooltip_font = create_font(self.pygame, 14, self.font_path) - self.sidebar_font = create_font(self.pygame, 14, self.font_path) - self.status_font = create_font(self.pygame, 18, self.font_path) - self.name_font = create_font(self.pygame, 16, self.font_path) + # 使用动态字体大小 + self.font = create_font(self.pygame, self.layout.font_size_medium, self.font_path) + self.tooltip_font = create_font(self.pygame, self.layout.font_size_tooltip, self.font_path) + self.sidebar_font = create_font(self.pygame, self.layout.font_size_normal, self.font_path) + self.status_font = create_font(self.pygame, self.layout.font_size_large, self.font_path) + self.name_font = create_font(self.pygame, self.layout.font_size_medium, self.font_path) self._region_font_cache: Dict[int, object] = {} self.colors = COLORS + # 使用动态尺寸加载资源 self.tile_images = load_tile_images(self.pygame, self.tile_size) self.tile_originals = load_tile_originals(self.pygame) self.sect_images = load_sect_images(self.pygame, self.tile_size) self.region_images = load_region_images(self.pygame, self.tile_size) - self.male_avatars, self.female_avatars = load_avatar_images(self.pygame, self.tile_size) + self.male_avatars, self.female_avatars = load_avatar_images(self.pygame, self.tile_size, self.layout.avatar_size) self.avatar_images: Dict[str, object] = {} self._assign_avatar_images() @@ -175,6 +199,7 @@ class Front: def _render(self): pygame = self.pygame self.screen.fill(self.colors["bg"]) + status_bar_height = self.layout.status_bar_height draw_map( pygame, self.screen, @@ -183,11 +208,11 @@ class Front: self.tile_images, self.tile_size, self.margin, - STATUS_BAR_HEIGHT, + status_bar_height, ) # 底图后叠加小区域整图(2x2/3x3),再绘制宗门总部,避免被覆盖 - 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) + 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( pygame, self.screen, @@ -196,7 +221,7 @@ class Front: self._get_region_font, self.tile_size, self.margin, - STATUS_BAR_HEIGHT, + status_bar_height, ) self._assign_avatar_images() hovered_default, hover_candidates = draw_avatars_and_pick_hover( @@ -208,13 +233,13 @@ class Front: self.tile_size, self.margin, self._get_display_center, - STATUS_BAR_HEIGHT, + status_bar_height, self.name_font, self._sidebar_filter_avatar_id, ) hovered_avatar = self._pick_hover_with_scroll(hovered_default, hover_candidates) # 先绘制状态栏和侧边栏,再绘制 tooltip 保证 tooltip 在最上层 - draw_status_bar(pygame, self.screen, self.colors, self.status_font, self.margin, self.world) + draw_status_bar(pygame, self.screen, self.colors, self.status_font, self.margin, self.world, status_bar_height) # 计算筛选后的事件 if self._sidebar_filter_avatar_id is None: @@ -233,7 +258,7 @@ class Front: sidebar_ui = draw_sidebar( pygame, self.screen, self.colors, self.sidebar_font, events_to_draw, - self.world.map, self.tile_size, self.margin, self.sidebar_width, + self.world.map, self.tile_size, self.margin, self.sidebar_width, status_bar_height, filter_selected_label=sel_label, filter_is_open=self._sidebar_filter_open, filter_options=options, @@ -241,7 +266,7 @@ class Front: # 保存供点击检测 self._sidebar_ui = sidebar_ui if hovered_avatar is not None: - draw_tooltip_for_avatar(pygame, self.screen, self.colors, self.tooltip_font, hovered_avatar) + draw_tooltip_for_avatar(pygame, self.screen, self.colors, self.tooltip_font, hovered_avatar, self.layout.tooltip_min_width, status_bar_height) # 绘制候选徽标(仅当存在多个候选) if len(hover_candidates) >= 2: from .rendering import draw_hover_badge @@ -253,10 +278,10 @@ class Front: idx = self._hover_candidates.index(hovered_avatar.id) except ValueError: idx = 0 - draw_hover_badge(pygame, self.screen, self.colors, self.tooltip_font, cx, cy, idx + 1, len(hover_candidates), STATUS_BAR_HEIGHT) + draw_hover_badge(pygame, self.screen, self.colors, self.tooltip_font, cx, cy, idx + 1, len(hover_candidates), status_bar_height) elif hovered_region is not None: mouse_x, mouse_y = pygame.mouse.get_pos() - draw_tooltip_for_region(pygame, self.screen, self.colors, self.tooltip_font, hovered_region, mouse_x, mouse_y) + draw_tooltip_for_region(pygame, self.screen, self.colors, self.tooltip_font, hovered_region, mouse_x, mouse_y, self.layout.tooltip_min_width, status_bar_height) # 绘制暂停菜单(在最上层) self._menu_option_rects = self.pause_menu.draw(self.screen, self.colors, self.status_font) diff --git a/src/front/assets.py b/src/front/assets.py index 4b56133..0143090 100644 --- a/src/front/assets.py +++ b/src/front/assets.py @@ -33,7 +33,18 @@ def load_tile_originals(pygame_mod) -> Dict[TileType, object]: return originals -def load_avatar_images(pygame_mod, tile_size: int): +def load_avatar_images(pygame_mod, tile_size: int, avatar_size: int = None): + """ + 加载avatar图像 + + Args: + pygame_mod: pygame模块 + tile_size: tile大小(用于计算默认avatar大小) + avatar_size: 可选,直接指定avatar大小;如果为None则根据tile_size计算 + """ + if avatar_size is None: + avatar_size = max(26, int((tile_size * 4 // 3) * 1.8)) + def load_from_dir(base_dir: str) -> List[object]: results: List[object] = [] if os.path.exists(base_dir): @@ -41,7 +52,6 @@ def load_avatar_images(pygame_mod, tile_size: int): if filename.endswith('.png') and filename != 'original.png' and filename.replace('.png', '').isdigit(): image_path = os.path.join(base_dir, filename) image = pygame_mod.image.load(image_path) - avatar_size = max(26, int((tile_size * 4 // 3) * 1.8)) scaled = pygame_mod.transform.scale(image, (avatar_size, avatar_size)) results.append(scaled) return results diff --git a/src/front/events_panel.py b/src/front/events_panel.py index d0a381b..35ceaa0 100644 --- a/src/front/events_panel.py +++ b/src/front/events_panel.py @@ -1,5 +1,5 @@ from typing import List, Optional, Tuple, Dict -from .rendering import STATUS_BAR_HEIGHT, map_pixel_size +from .rendering import map_pixel_size def _wrap_text_by_pixels(font, text: str, max_width_px: int) -> List[str]: @@ -35,6 +35,7 @@ def draw_sidebar( tile_size: int, margin: int, sidebar_width: int, + status_bar_height: int, *, filter_selected_label: str, filter_is_open: bool, @@ -42,7 +43,7 @@ def draw_sidebar( ) -> Dict[str, object]: 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_y = margin + status_bar_height sidebar_rect = pygame_mod.Rect( sidebar_x, diff --git a/src/front/layout.py b/src/front/layout.py new file mode 100644 index 0000000..9a90625 --- /dev/null +++ b/src/front/layout.py @@ -0,0 +1,137 @@ +"""动态布局计算模块 + +根据屏幕分辨率动态计算所有UI组件的尺寸,实现自适应布局。 +""" +from typing import NamedTuple + + +class LayoutConfig(NamedTuple): + """布局配置类,包含所有动态计算的尺寸参数""" + + # 屏幕尺寸 + screen_width: int + screen_height: int + + # 地图相关 + tile_size: int + margin: int + + # UI组件尺寸 + status_bar_height: int + sidebar_width: int + + # 字体尺寸 + font_size_normal: int # 普通文本(原14) + font_size_medium: int # 中等文本(原16) + font_size_large: int # 大文本(原18) + font_size_tooltip: int # tooltip文本(原14) + + # 其他动态参数 + avatar_size: int # avatar图像大小 + tooltip_min_width: int # tooltip最小宽度 + + +def clamp(value: float, min_val: float, max_val: float) -> int: + """将值限制在指定范围内并返回整数""" + return int(max(min_val, min(max_val, value))) + + +def calculate_layout(screen_width: int, screen_height: int, map_width: int = 56, map_height: int = 40) -> LayoutConfig: + """ + 根据屏幕分辨率计算所有布局参数 + + Args: + screen_width: 屏幕宽度(像素) + screen_height: 屏幕高度(像素) + map_width: 地图宽度(格子数) + map_height: 地图高度(格子数) + + Returns: + LayoutConfig: 包含所有布局参数的配置对象 + """ + + # 1. 计算固定UI组件尺寸(使用混合策略:百分比 + 最小最大值限制) + + # 状态栏高度:屏幕高度的2.5%,限制在24-48px + status_bar_height = clamp(screen_height * 0.025, 24, 48) + + # 侧边栏宽度:屏幕宽度的18%,限制在280-420px + sidebar_width = clamp(screen_width * 0.18, 280, 420) + + # 边距:屏幕较短边的0.8%,限制在6-16px + margin = clamp(min(screen_width, screen_height) * 0.008, 6, 16) + + # 2. 计算地图区域可用空间 + available_width = screen_width - sidebar_width - margin * 2 + available_height = screen_height - status_bar_height - margin * 2 + + # 3. 计算tile_size(保证完整显示地图) + tile_size_by_width = available_width / map_width + tile_size_by_height = available_height / map_height + + # 取较小值确保两个方向都能完整显示,并限制最大值为64px(防止超大屏幕显示异常) + tile_size = clamp(min(tile_size_by_width, tile_size_by_height), 1, 64) + + # 4. 计算字体尺寸(根据tile_size动态缩放) + # 基准:tile_size=32时,字体大小为 14/16/18 + font_scale = tile_size / 32.0 + + font_size_normal = max(14, int(14 * font_scale)) # 最小14px保证可读性 + font_size_medium = max(16, int(16 * font_scale)) + font_size_large = max(18, int(18 * font_scale)) + font_size_tooltip = max(14, int(14 * font_scale)) + + # 5. 计算avatar尺寸(与原来的公式保持一致,但基于动态tile_size) + avatar_size = max(20, int((tile_size * 4 // 3) * 1.8)) + + # 6. 计算tooltip最小宽度(原来是260px,按比例缩放) + tooltip_min_width = max(200, int(260 * font_scale)) + + return LayoutConfig( + screen_width=screen_width, + screen_height=screen_height, + tile_size=tile_size, + margin=margin, + status_bar_height=status_bar_height, + sidebar_width=sidebar_width, + font_size_normal=font_size_normal, + font_size_medium=font_size_medium, + font_size_large=font_size_large, + font_size_tooltip=font_size_tooltip, + avatar_size=avatar_size, + tooltip_min_width=tooltip_min_width, + ) + + +def get_fullscreen_resolution(pygame_mod) -> tuple[int, int]: + """ + 获取当前显示器的可用分辨率(排除任务栏) + + Args: + pygame_mod: pygame模块 + + Returns: + (width, height): 可用屏幕分辨率 + """ + # 初始化video模块(如果还未初始化) + if not pygame_mod.get_init(): + pygame_mod.init() + + # 获取显示器信息 + info = pygame_mod.display.Info() + + # 获取桌面可用区域(排除任务栏等) + # 在Windows上,current_w/h 是全屏分辨率 + # 我们需要预留一些空间给任务栏(通常在底部约40-60像素) + width = info.current_w + height = info.current_h + + # 为任务栏预留空间(约40-50像素,取决于缩放比例) + # 这是一个保守的估计,确保窗口不会覆盖任务栏 + taskbar_height = max(40, int(height * 0.04)) # 约4%的高度 + + return width, height - taskbar_height + + +__all__ = ["LayoutConfig", "calculate_layout", "get_fullscreen_resolution"] + diff --git a/src/front/rendering.py b/src/front/rendering.py index 6008700..9aeb38c 100644 --- a/src/front/rendering.py +++ b/src/front/rendering.py @@ -3,9 +3,7 @@ from typing import List, Optional, Tuple, Callable 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 行进行字符级换行: @@ -335,13 +333,12 @@ 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, top_limit: int = 0): +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): padding = 6 spacing = 2 surf_lines = [font.render(t, True, colors["text"]) for t in lines] width = max(s.get_width() for s in surf_lines) + padding * 2 - if min_width is not None: - width = max(width, min_width) + width = max(width, min_width) height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1) x = mouse_x + 12 y = mouse_y + 12 @@ -362,19 +359,19 @@ def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mou cursor_y += s.get_height() + spacing -def draw_tooltip_for_avatar(pygame_mod, screen, colors, font, avatar: Avatar): +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 获取信息行,避免前端重复拼接 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) + 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): +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): if region is None: return # 改为调用 region.get_hover_info(),并统一用 wrap_lines_for_tooltip 进行换行 lines = region.get_hover_info() 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) + 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): @@ -394,13 +391,12 @@ def draw_year_month_info(pygame_mod, screen, colors, font, margin: int, guide_wi screen.blit(ym_surf, (x_pos, 8)) -def draw_status_bar(pygame_mod, screen, colors, font, margin: int, world): +def draw_status_bar(pygame_mod, screen, colors, font, margin: int, world, status_bar_height: int = 32): status_y = 8 - status_height = STATUS_BAR_HEIGHT - status_rect = pygame_mod.Rect(0, 0, screen.get_width(), status_height) + status_rect = pygame_mod.Rect(0, 0, screen.get_width(), status_bar_height) pygame_mod.draw.rect(screen, colors["status_bg"], status_rect) pygame_mod.draw.line(screen, colors["status_border"], - (0, status_height), (screen.get_width(), status_height), 2) + (0, status_bar_height), (screen.get_width(), status_bar_height), 2) guide_w = draw_operation_guide(pygame_mod, screen, colors, font, margin) draw_year_month_info(pygame_mod, screen, colors, font, margin, guide_w, world) @@ -412,8 +408,10 @@ __all__ = [ "draw_tooltip_for_avatar", "draw_tooltip_for_region", "draw_status_bar", - "STATUS_BAR_HEIGHT", "map_pixel_size", + "draw_hover_badge", + "draw_small_regions", + "draw_sect_headquarters", ] diff --git a/src/run/run.py b/src/run/run.py index 929a5ac..20f4594 100644 --- a/src/run/run.py +++ b/src/run/run.py @@ -101,11 +101,8 @@ async def main(): front = Front( simulator=sim, - tile_size=24, - margin=8, step_interval_ms=750, window_title="Cultivation World — Front Demo", - sidebar_width=350, ) await front.run_async()