add save and load func

This commit is contained in:
bridge
2025-11-11 19:48:18 +08:00
parent 0cb7eacee7
commit 9b870475bf
21 changed files with 1348 additions and 32 deletions

View File

@@ -22,6 +22,7 @@ from .rendering import (
)
from .events_panel import draw_sidebar
from .menu import PauseMenu
from .toast import Toast
from .layout import calculate_layout, get_fullscreen_resolution
@@ -33,12 +34,14 @@ class Front:
step_interval_ms: int = 400,
window_title: str = "Cultivation World Simulator",
font_path: Optional[str] = None,
existed_sects: Optional[List] = None,
):
self.world = simulator.world
self.simulator = simulator
self.step_interval_ms = step_interval_ms
self.window_title = window_title
self.font_path = font_path
self.existed_sects = existed_sects or [] # 保存本局启用的宗门列表
self._last_step_ms = 0
self.events: List[Event] = []
@@ -110,6 +113,12 @@ class Front:
# 暂停菜单
self.pause_menu = PauseMenu(pygame)
# Toast提示
self.toast = Toast(pygame)
# 世界ID标记用于取消过期的异步任务
self._world_id = 0
# 渲染插值状态avatar_id -> {start_px, start_py, target_px, target_py, start_ms, duration_ms}
self._avatar_display_states: Dict[str, Dict[str, float]] = {}
@@ -135,7 +144,16 @@ class Front:
self.events = self.events[-1000:]
async def _step_once_async(self):
# 捕获当前world_id用于检测是否已经加载了新世界
current_world_id = self._world_id
events = await self.simulator.step()
# 如果world_id已改变说明加载了新存档丢弃这次结果
if self._world_id != current_world_id:
print(f"丢弃过期的异步任务结果world_id: {current_world_id} -> {self._world_id}")
return
if events:
self.add_events(events)
self._last_step_ms = 0
@@ -162,12 +180,13 @@ class Front:
if event.key == pygame.K_ESCAPE:
self.pause_menu.toggle()
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
# 处理菜单点击
# 处理菜单点击(菜单可见时阻止其他所有交互)
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):
@@ -213,31 +232,38 @@ class Front:
# 底图后叠加小区域整图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)
hovered_region = draw_region_labels(
pygame,
self.screen,
self.colors,
self.world,
self._get_region_font,
self.tile_size,
self.margin,
status_bar_height,
)
self._assign_avatar_images()
hovered_default, hover_candidates = draw_avatars_and_pick_hover(
pygame,
self.screen,
self.colors,
self.simulator,
self.avatar_images,
self.tile_size,
self.margin,
self._get_display_center,
status_bar_height,
self.name_font,
self._sidebar_filter_avatar_id,
)
hovered_avatar = self._pick_hover_with_scroll(hovered_default, hover_candidates)
# 如果菜单可见不显示任何hover避免穿透
if not self.pause_menu.is_visible:
hovered_region = draw_region_labels(
pygame,
self.screen,
self.colors,
self.world,
self._get_region_font,
self.tile_size,
self.margin,
status_bar_height,
)
self._assign_avatar_images()
hovered_default, hover_candidates = draw_avatars_and_pick_hover(
pygame,
self.screen,
self.colors,
self.simulator,
self.avatar_images,
self.tile_size,
self.margin,
self._get_display_center,
status_bar_height,
self.name_font,
self._sidebar_filter_avatar_id,
)
hovered_avatar = self._pick_hover_with_scroll(hovered_default, hover_candidates)
else:
# 菜单可见时清空所有hover状态
hovered_region = None
hovered_avatar = None
hover_candidates = []
# 先绘制状态栏和侧边栏,再绘制 tooltip 保证 tooltip 在最上层
draw_status_bar(pygame, self.screen, self.colors, self.status_font, self.margin, self.world, status_bar_height)
@@ -286,6 +312,10 @@ class Front:
# 绘制暂停菜单(在最上层)
self._menu_option_rects = self.pause_menu.draw(self.screen, self.colors, self.status_font)
# 更新并绘制Toast在最上层
self.toast.update()
self.toast.draw(self.screen, self.sidebar_font)
pygame.display.flip()
def _handle_mouse_click(self) -> None:
@@ -309,7 +339,121 @@ class Front:
"""处理菜单点击,返回动作"""
mouse_pos = self.pygame.mouse.get_pos()
option_rects = getattr(self, "_menu_option_rects", [])
return self.pause_menu.handle_click(mouse_pos, option_rects)
action = self.pause_menu.handle_click(mouse_pos, option_rects)
# 处理保存和加载操作
if action == "save":
self._save_game()
self.pause_menu.hide()
return None
elif action == "load":
success = self._load_game()
if success:
self.pause_menu.hide()
return None
return action
def _save_game(self) -> bool:
"""保存游戏"""
try:
from src.sim.save.save_game import save_game
success, filename = save_game(self.world, self.simulator, self.existed_sects)
if success and filename:
self.toast.show(f"保存成功!\n{filename}", Toast.SUCCESS, duration_ms=4000)
print(f"游戏保存成功!文件:{filename}")
else:
self.toast.show("游戏保存失败", Toast.ERROR)
return success
except Exception as e:
self.toast.show(f"保存失败: {str(e)[:30]}", Toast.ERROR)
print(f"保存游戏时出错: {e}")
import traceback
traceback.print_exc()
return False
def _load_game(self) -> bool:
"""加载游戏 - 打开文件选择对话框"""
try:
import tkinter as tk
from tkinter import filedialog
from pathlib import Path
from src.utils.config import CONFIG
from src.sim.load.load_game import load_game
# 创建临时的tkinter根窗口隐藏
root = tk.Tk()
root.withdraw()
root.attributes('-topmost', True)
# 获取saves目录
saves_dir = CONFIG.paths.saves
saves_dir.mkdir(parents=True, exist_ok=True)
# 打开文件选择对话框
save_path = filedialog.askopenfilename(
title="选择存档文件",
initialdir=str(saves_dir),
filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")]
)
# 销毁tkinter根窗口
root.destroy()
# 如果用户取消
if not save_path:
self.toast.show("取消加载", Toast.INFO, duration_ms=2000)
return False
save_path = Path(save_path)
if not save_path.exists():
self.toast.show("存档文件不存在", Toast.ERROR)
return False
# 显示加载提示
self.toast.show("正在加载存档...", Toast.INFO, duration_ms=10000)
# 强制刷新一次屏幕让toast显示出来
self._render()
# 加载游戏数据
world, simulator, existed_sects = load_game(save_path)
# 增加world_id使所有正在进行的异步任务失效
self._world_id += 1
# 替换当前的world和simulator
self.world = world
self.simulator = simulator
self.existed_sects = existed_sects
# 从event_manager恢复事件到侧边栏显示列表
self.events.clear()
recent_events = world.event_manager.get_recent_events(limit=1000)
self.events.extend(recent_events)
# 重新初始化头像图像分配
self.avatar_images.clear()
self._assign_avatar_images()
# 重新初始化插值状态
self._avatar_display_states.clear()
self._init_avatar_display_states()
# 标记侧栏选项为脏(需要重建角色列表)
self._sidebar_options_dirty = True
self._sidebar_filter_avatar_id = None
# 立即显示成功toast覆盖"正在加载"的toast
filename = save_path.name
self.toast.show(f"加载成功!\n{filename}", Toast.SUCCESS, duration_ms=3000)
print(f"游戏加载成功!文件:{filename}")
return True
except Exception as e:
self.toast.show(f"加载失败: {str(e)[:30]}", Toast.ERROR)
print(f"加载游戏时出错: {e}")
import traceback
traceback.print_exc()
return False
def _get_region_font(self, size: int):
return _get_region_font_cached(self.pygame, self._region_font_cache, size, self.font_path)

View File

@@ -15,6 +15,8 @@ class PauseMenu:
self.pygame = pygame_mod
self.is_visible = False
self.options = [
MenuOption("保存游戏", "save"),
MenuOption("加载游戏", "load"),
MenuOption("退出游戏", "quit")
]
self.selected_index = 0
@@ -50,9 +52,9 @@ class PauseMenu:
pygame = self.pygame
screen_w, screen_h = screen.get_size()
# 绘制半透明黑色背景(模糊效果
# 绘制全屏半透明黑色背景(作为mask阻止背后交互
overlay = pygame.Surface((screen_w, screen_h), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 160))
overlay.fill((0, 0, 0, 180))
screen.blit(overlay, (0, 0))
# 计算菜单尺寸

149
src/front/toast.py Normal file
View File

@@ -0,0 +1,149 @@
"""Toast提示组件
用于显示临时的成功/失败/信息提示
"""
from typing import Optional
class Toast:
"""Toast提示
显示短暂的提示信息,自动消失
"""
# Toast类型
SUCCESS = "success"
ERROR = "error"
INFO = "info"
def __init__(self, pygame_mod):
self.pygame = pygame_mod
self.message: Optional[str] = None
self.toast_type: str = Toast.INFO
self.start_time: int = 0
self.duration_ms: int = 3000 # 默认显示3秒
self.is_visible: bool = False
def show(self, message: str, toast_type: str = INFO, duration_ms: int = 3000):
"""显示Toast提示
Args:
message: 提示信息
toast_type: 类型success/error/info
duration_ms: 显示时长(毫秒)
"""
self.message = message
self.toast_type = toast_type
self.duration_ms = duration_ms
self.start_time = self.pygame.time.get_ticks()
self.is_visible = True
def update(self):
"""更新Toast状态检查是否应该隐藏"""
if not self.is_visible:
return
current_time = self.pygame.time.get_ticks()
if current_time - self.start_time >= self.duration_ms:
self.is_visible = False
self.message = None
def draw(self, screen, font):
"""绘制Toast
Args:
screen: pygame屏幕对象
font: pygame字体对象
"""
if not self.is_visible or not self.message:
return
pygame = self.pygame
screen_w, screen_h = screen.get_size()
# 根据类型选择颜色
if self.toast_type == Toast.SUCCESS:
bg_color = (34, 139, 34) # 绿色
border_color = (46, 184, 46)
elif self.toast_type == Toast.ERROR:
bg_color = (178, 34, 34) # 红色
border_color = (220, 50, 50)
else: # INFO
bg_color = (70, 130, 180) # 蓝色
border_color = (100, 150, 200)
# 计算淡入淡出效果
elapsed = self.pygame.time.get_ticks() - self.start_time
fade_in_duration = 200 # 淡入200ms
fade_out_duration = 500 # 淡出500ms
if elapsed < fade_in_duration:
# 淡入阶段
alpha = int(255 * (elapsed / fade_in_duration))
elif elapsed > self.duration_ms - fade_out_duration:
# 淡出阶段
remaining = self.duration_ms - elapsed
alpha = int(255 * (remaining / fade_out_duration))
else:
# 完全显示
alpha = 255
# 创建更大的字体用于Toast
from .fonts import create_font
toast_font = create_font(pygame, 24, None) # 使用24号字体更大更清晰
# 处理多行文本
lines = self.message.split('\n')
text_surfaces = []
max_text_w = 0
total_text_h = 0
line_spacing = 5 # 行间距
for line in lines:
text_surf = toast_font.render(line, True, (255, 255, 255))
text_surfaces.append(text_surf)
w, h = text_surf.get_size()
max_text_w = max(max_text_w, w)
total_text_h += h
# 加上行间距
total_text_h += line_spacing * (len(lines) - 1)
# Toast尺寸增大padding
padding_x = 40 # 水平padding增大
padding_y = 25 # 垂直padding增大
toast_w = max(max_text_w + padding_x * 2, 300) # 最小宽度300
toast_h = total_text_h + padding_y * 2
# 位置:屏幕上方中央
toast_x = (screen_w - toast_w) // 2
toast_y = 100 # 稍微下移一点
# 创建带透明度的surface
toast_surface = pygame.Surface((toast_w, toast_h), pygame.SRCALPHA)
# 绘制背景(带圆角和透明度)
bg_with_alpha = (*bg_color, alpha)
pygame.draw.rect(toast_surface, bg_with_alpha, (0, 0, toast_w, toast_h), border_radius=8)
# 绘制边框
border_with_alpha = (*border_color, alpha)
pygame.draw.rect(toast_surface, border_with_alpha, (0, 0, toast_w, toast_h), 2, border_radius=8)
# 绘制多行文本(应用透明度,居中显示)
current_y = (toast_h - total_text_h) // 2 # 垂直居中起点
for text_surf in text_surfaces:
w, h = text_surf.get_size()
text_with_alpha = pygame.Surface((w, h), pygame.SRCALPHA)
text_with_alpha.blit(text_surf, (0, 0))
text_with_alpha.set_alpha(alpha)
text_x = (toast_w - w) // 2 # 每行水平居中
toast_surface.blit(text_with_alpha, (text_x, current_y))
current_y += h + line_spacing
# 绘制到屏幕
screen.blit(toast_surface, (toast_x, toast_y))
__all__ = ["Toast"]