This commit is contained in:
bridge
2025-11-02 20:44:05 +08:00
parent a2c826a970
commit 272ba8d9d8
3 changed files with 153 additions and 21 deletions

View File

@@ -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)

109
src/front/menu.py Normal file
View File

@@ -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"]

View File

@@ -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)