diff --git a/src/front/app.py b/src/front/app.py index 9cf266f..69325b3 100644 --- a/src/front/app.py +++ b/src/front/app.py @@ -21,6 +21,7 @@ from .rendering import ( draw_sect_headquarters, ) from .events_panel import draw_sidebar +from .menu import PauseMenu class Front: @@ -44,7 +45,6 @@ class Front: self.font_path = font_path self.sidebar_width = sidebar_width - self._auto_step = True self._last_step_ms = 0 self.events: List[Event] = [] @@ -76,6 +76,9 @@ class Front: self._assign_avatar_images() self.clock = pygame.time.Clock() + + # 暂停菜单 + self.pause_menu = PauseMenu(pygame) # 渲染插值状态:avatar_id -> {start_px, start_py, target_px, target_py, start_ms, duration_ms} self._avatar_display_states: Dict[str, Dict[str, float]] = {} @@ -116,37 +119,48 @@ class Front: current_step_task = None while running: dt_ms = self.clock.tick(60) - self._last_step_ms += dt_ms + + # 游戏未暂停时才累积时间 + if not self.pause_menu.is_visible: + self._last_step_ms += dt_ms + for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: - running = False - elif event.key == pygame.K_a: - self._auto_step = not self._auto_step - elif event.key == pygame.K_SPACE: - if current_step_task is None or current_step_task.done(): - current_step_task = asyncio.create_task(self._step_once_async()) + self.pause_menu.toggle() elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: - self._handle_mouse_click() + # 处理菜单点击 + if self.pause_menu.is_visible: + action = self._handle_menu_click() + if action == "quit": + running = False + else: + self._handle_mouse_click() # 兼容旧版滚轮为 MOUSEBUTTON 4/5 elif event.type == pygame.MOUSEBUTTONDOWN and event.button in (4, 5): - delta = 1 if event.button == 4 else -1 - self._on_mouse_wheel(delta) + if not self.pause_menu.is_visible: + delta = 1 if event.button == 4 else -1 + self._on_mouse_wheel(delta) # pygame 2 的标准滚轮事件 elif getattr(pygame, "MOUSEWHEEL", None) is not None and event.type == pygame.MOUSEWHEEL: - # event.y: 上滚为正,下滚为负 - self._on_mouse_wheel(int(getattr(event, "y", 0))) - if self._auto_step and self._last_step_ms >= self.step_interval_ms: + if not self.pause_menu.is_visible: + # event.y: 上滚为正,下滚为负 + self._on_mouse_wheel(int(getattr(event, "y", 0))) + + # 游戏未暂停时才自动步进 + if not self.pause_menu.is_visible and self._last_step_ms >= self.step_interval_ms: if current_step_task is None or current_step_task.done(): current_step_task = asyncio.create_task(self._step_once_async()) self._last_step_ms = 0 + if current_step_task and current_step_task.done(): await current_step_task current_step_task = None # 再次确保目标同步(防止外部触发的状态变更遗漏) self._update_avatar_display_targets() + self._render() await asyncio.sleep(0.016) pygame.quit() @@ -193,7 +207,7 @@ class Front: ) 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, self._auto_step) + draw_status_bar(pygame, self.screen, self.colors, self.status_font, self.margin, self.world) # 计算筛选后的事件 if self._sidebar_filter_avatar_id is None: @@ -236,10 +250,14 @@ class Front: 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) + + # 绘制暂停菜单(在最上层) + self._menu_option_rects = self.pause_menu.draw(self.screen, self.colors, self.status_font) + pygame.display.flip() def _handle_mouse_click(self) -> None: - # 仅处理侧栏筛选点击 + """处理侧栏筛选点击""" pygame = self.pygame mouse_pos = pygame.mouse.get_pos() ui = getattr(self, "_sidebar_ui", {}) or {} @@ -254,6 +272,12 @@ class Front: self._sidebar_filter_avatar_id = oid if oid is not None else None self._sidebar_filter_open = False return + + def _handle_menu_click(self) -> Optional[str]: + """处理菜单点击,返回动作""" + mouse_pos = self.pygame.mouse.get_pos() + option_rects = getattr(self, "_menu_option_rects", []) + return self.pause_menu.handle_click(mouse_pos, option_rects) def _get_region_font(self, size: int): return _get_region_font_cached(self.pygame, self._region_font_cache, size, self.font_path) diff --git a/src/front/menu.py b/src/front/menu.py new file mode 100644 index 0000000..df3640a --- /dev/null +++ b/src/front/menu.py @@ -0,0 +1,109 @@ +"""游戏暂停菜单""" +from typing import Optional, Tuple + + +class MenuOption: + """菜单选项""" + def __init__(self, label: str, action: str): + self.label = label + self.action = action + + +class PauseMenu: + """暂停菜单""" + def __init__(self, pygame_mod): + self.pygame = pygame_mod + self.is_visible = False + self.options = [ + MenuOption("退出游戏", "quit") + ] + self.selected_index = 0 + + def toggle(self): + """切换菜单显示状态""" + self.is_visible = not self.is_visible + self.selected_index = 0 + + def show(self): + """显示菜单""" + self.is_visible = True + + def hide(self): + """隐藏菜单""" + self.is_visible = False + + def handle_click(self, mouse_pos: Tuple[int, int], option_rects: list) -> Optional[str]: + """处理鼠标点击,返回被点击的选项动作""" + if not self.is_visible: + return None + + for i, rect in enumerate(option_rects): + if rect.collidepoint(mouse_pos): + return self.options[i].action + return None + + def draw(self, screen, colors, font): + """绘制菜单""" + if not self.is_visible: + return [] + + pygame = self.pygame + screen_w, screen_h = screen.get_size() + + # 绘制半透明黑色背景(模糊效果) + overlay = pygame.Surface((screen_w, screen_h), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 160)) + screen.blit(overlay, (0, 0)) + + # 计算菜单尺寸 + padding = 40 + option_height = 50 + option_spacing = 20 + menu_width = 300 + menu_height = padding * 2 + len(self.options) * option_height + (len(self.options) - 1) * option_spacing + + # 菜单居中位置 + menu_x = (screen_w - menu_width) // 2 + menu_y = (screen_h - menu_height) // 2 + + # 绘制菜单背景 + menu_rect = pygame.Rect(menu_x, menu_y, menu_width, menu_height) + pygame.draw.rect(screen, (40, 40, 40), menu_rect, border_radius=12) + pygame.draw.rect(screen, (100, 100, 100), menu_rect, 2, border_radius=12) + + # 绘制选项 + option_rects = [] + current_y = menu_y + padding + + for i, option in enumerate(self.options): + option_rect = pygame.Rect( + menu_x + 30, + current_y, + menu_width - 60, + option_height + ) + + # 检测鼠标悬停 + mouse_pos = pygame.mouse.get_pos() + is_hovered = option_rect.collidepoint(mouse_pos) + + # 绘制选项背景 + bg_color = (80, 80, 80) if is_hovered else (50, 50, 50) + pygame.draw.rect(screen, bg_color, option_rect, border_radius=8) + pygame.draw.rect(screen, (120, 120, 120), option_rect, 1, border_radius=8) + + # 绘制选项文本 + text_color = (255, 255, 255) if is_hovered else (200, 200, 200) + text_surf = font.render(option.label, True, text_color) + text_x = option_rect.centerx - text_surf.get_width() // 2 + text_y = option_rect.centery - text_surf.get_height() // 2 + screen.blit(text_surf, (text_x, text_y)) + + option_rects.append(option_rect) + current_y += option_height + option_spacing + + return option_rects + + +__all__ = ["PauseMenu"] + diff --git a/src/front/rendering.py b/src/front/rendering.py index fb2f3ee..6008700 100644 --- a/src/front/rendering.py +++ b/src/front/rendering.py @@ -377,9 +377,8 @@ def draw_tooltip_for_region(pygame_mod, screen, colors, font, region, mouse_x: i 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): - auto_status = "开" if auto_step else "关" - guide_text = f"A:自动步进({auto_status}) SPACE:单步 ESC:退出" +def draw_operation_guide(pygame_mod, screen, colors, font, margin: int): + guide_text = "ESC: 呼出菜单" guide_surf = font.render(guide_text, True, colors["status_text"]) x_pos = margin + 8 screen.blit(guide_surf, (x_pos, 8)) @@ -395,14 +394,14 @@ 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, auto_step: bool): +def draw_status_bar(pygame_mod, screen, colors, font, margin: int, world): status_y = 8 status_height = STATUS_BAR_HEIGHT status_rect = pygame_mod.Rect(0, 0, screen.get_width(), status_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) - guide_w = draw_operation_guide(pygame_mod, screen, colors, font, margin, auto_step) + guide_w = draw_operation_guide(pygame_mod, screen, colors, font, margin) draw_year_month_info(pygame_mod, screen, colors, font, margin, guide_w, world)