add root
This commit is contained in:
@@ -7,6 +7,8 @@ from src.classes.calendar import Month, Year
|
||||
from src.classes.action import Action
|
||||
from src.classes.world import World
|
||||
from src.classes.tile import Tile
|
||||
from src.classes.cultivation import CultivationProgress
|
||||
from src.classes.root import Root
|
||||
from src.utils.strings import to_snake_case
|
||||
|
||||
class Gender(Enum):
|
||||
@@ -34,10 +36,12 @@ class Avatar:
|
||||
birth_year: Year
|
||||
age: int
|
||||
gender: Gender
|
||||
cultivation_progress: CultivationProgress = field(default_factory=lambda: CultivationProgress(0))
|
||||
pos_x: int = 0
|
||||
pos_y: int = 0
|
||||
tile: Optional[Tile] = None
|
||||
actions: dict[str, Action] = field(default_factory=dict)
|
||||
root: Root = field(default_factory=lambda: random.choice(list(Root)))
|
||||
|
||||
|
||||
def bind_action(self, action_class: type[Action]):
|
||||
|
||||
130
src/classes/cultivation.py
Normal file
130
src/classes/cultivation.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from enum import Enum
|
||||
|
||||
class Realm(Enum):
|
||||
Qi_Refinement = "练气"
|
||||
Foundation_Establishment = "筑基"
|
||||
Core_Formation = "金丹"
|
||||
Nascent_Soul = "元婴"
|
||||
|
||||
class Stage(Enum):
|
||||
Early_Stage = "前期"
|
||||
Middle_Stage = "中期"
|
||||
Late_Stage = "后期"
|
||||
|
||||
levels_per_realm = 30
|
||||
levels_per_stage = 10
|
||||
|
||||
level_to_realm = {
|
||||
0: Realm.Qi_Refinement,
|
||||
30: Realm.Foundation_Establishment,
|
||||
60: Realm.Core_Formation,
|
||||
90: Realm.Nascent_Soul,
|
||||
}
|
||||
level_to_stage = {
|
||||
0: Stage.Early_Stage,
|
||||
10: Stage.Middle_Stage,
|
||||
20: Stage.Late_Stage,
|
||||
}
|
||||
|
||||
class CultivationProgress:
|
||||
"""
|
||||
修仙进度(包含等级、境界和经验值)
|
||||
目前一个四个大境界,每个境界分前期、中期、后期。每一期对应10级。
|
||||
所以每一个境界对应30级。境界的级别满了之后,需要突破才能进入下一个境界与升级。
|
||||
所以有:
|
||||
练气(Qi Refinement):前期(1-10)、中期(11-20)、后期(21-30)、突破(31)
|
||||
筑基(Foundation Establishment):前期(31-40)、中期(41-50)、后期(51-60)、突破(61)
|
||||
金丹(Core Formation):前期(61-70)、中期(71-80)、后期(81-90)、突破(91)
|
||||
元婴(Nascent Soul):前期(91-100)、中期(101-110)、后期(111-120)、突破(121)
|
||||
"""
|
||||
|
||||
def __init__(self, level: int, exp: int = 0):
|
||||
self.level = level
|
||||
self.exp = exp
|
||||
self.realm = self.get_realm(level)
|
||||
self.stage = self.get_stage(level)
|
||||
|
||||
def get_realm(self, level: int) -> str:
|
||||
"""获取境界"""
|
||||
for level_threshold, realm in reversed(list(level_to_realm.items())):
|
||||
if level >= level_threshold:
|
||||
return realm
|
||||
return Realm.Qi_Refinement
|
||||
|
||||
def get_stage(self, level: int) -> str:
|
||||
"""获取阶段"""
|
||||
_level = self.level % levels_per_realm
|
||||
for level_threshold, stage in reversed(list(level_to_stage.items())):
|
||||
if _level >= level_threshold:
|
||||
return stage
|
||||
return Stage.Early_Stage
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.realm.value}{self.stage.value}({self.level}级)"
|
||||
|
||||
def get_exp_required(self, target_level: int) -> int:
|
||||
"""
|
||||
计算升级到指定等级需要的经验值
|
||||
使用指数增长公式:base_exp * (growth_rate ^ level) * realm_multiplier
|
||||
|
||||
参数:
|
||||
target_level: 目标等级
|
||||
|
||||
返回:
|
||||
需要的经验值
|
||||
"""
|
||||
if target_level <= 0 or target_level > 120:
|
||||
return 0
|
||||
|
||||
base_exp = 100 # 基础经验值
|
||||
growth_rate = 1.15 # 每级增长15%
|
||||
|
||||
# 境界加成倍数:每跨越一个境界,经验需求增加50%
|
||||
realm_multiplier = 1 + (target_level // 30) * 0.5
|
||||
|
||||
exp_required = int(base_exp * (growth_rate ** target_level) * realm_multiplier)
|
||||
return exp_required
|
||||
|
||||
def can_level_up(self) -> bool:
|
||||
"""
|
||||
检查是否可以升级
|
||||
|
||||
返回:
|
||||
如果经验值足够升级则返回True
|
||||
"""
|
||||
required_exp = self.get_exp_required(self.level + 1)
|
||||
return self.exp >= required_exp
|
||||
|
||||
def get_exp_progress(self) -> tuple[int, int]:
|
||||
"""
|
||||
获取当前经验值进度
|
||||
|
||||
返回:
|
||||
(当前经验值, 升级所需经验值)
|
||||
"""
|
||||
required_exp = self.get_exp_required(self.level + 1)
|
||||
return self.exp, required_exp
|
||||
|
||||
def add_exp(self, exp_amount: int) -> bool:
|
||||
"""
|
||||
增加经验值
|
||||
|
||||
参数:
|
||||
exp_amount: 要增加的经验值数量
|
||||
|
||||
返回:
|
||||
如果升级了则返回True
|
||||
"""
|
||||
self.exp += exp_amount
|
||||
|
||||
# 检查是否可以升级
|
||||
while self.can_level_up():
|
||||
required_exp = self.get_exp_required()
|
||||
self.exp -= required_exp
|
||||
self.level += 1
|
||||
# 更新境界和阶段
|
||||
self.realm = self.get_realm(self.level)
|
||||
self.stage = self.get_stage(self.level)
|
||||
return True
|
||||
|
||||
return False
|
||||
17
src/classes/root.py
Normal file
17
src/classes/root.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
灵根
|
||||
目前只有五行灵根,金木水火土。
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Root(Enum):
|
||||
"""
|
||||
灵根
|
||||
"""
|
||||
Metal = "金"
|
||||
Wood = "木"
|
||||
Water = "水"
|
||||
Fire = "火"
|
||||
Earth = "土"
|
||||
@@ -11,15 +11,16 @@ from src.classes.avatar import Avatar, Gender
|
||||
class Front:
|
||||
"""
|
||||
基于 pygame 的前端展示。
|
||||
|
||||
- 渲染地图 `World.map` 与其中的 `Avatar`
|
||||
- 以固定节奏调用 `simulator.step()`,画面随之更新
|
||||
- 鼠标悬停在 avatar 上时显示信息
|
||||
|
||||
|
||||
功能:
|
||||
- 渲染地图与Avatar
|
||||
- 自动/手动步进模拟
|
||||
- 鼠标悬停显示信息
|
||||
|
||||
按键:
|
||||
- A:切换自动步进(默认开启)
|
||||
- 空格:手动执行一步(在自动关闭时有用)
|
||||
- ESC / 关闭窗口:退出
|
||||
- A:切换自动步进
|
||||
- 空格:手动执行一步
|
||||
- ESC:退出
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -45,9 +46,8 @@ class Front:
|
||||
self._auto_step = True
|
||||
self._last_step_ms = 0
|
||||
|
||||
# 延迟导入 pygame:避免未安装 pygame 时影响非可视化运行/测试
|
||||
import pygame # type: ignore
|
||||
|
||||
# 初始化pygame
|
||||
import pygame
|
||||
self.pygame = pygame
|
||||
pygame.init()
|
||||
pygame.font.init()
|
||||
@@ -58,14 +58,13 @@ class Front:
|
||||
self.screen = pygame.display.set_mode((width_px, height_px))
|
||||
pygame.display.set_caption(window_title)
|
||||
|
||||
# 字体(优先中文友好字体;可显式传入 TTF 路径)
|
||||
# 字体和缓存
|
||||
self.font = self._create_font(16)
|
||||
self.tooltip_font = self._create_font(14)
|
||||
# 区域名字体缓存:按需动态放大(随区域面积和格子大小自适应)
|
||||
self._region_font_cache: Dict[int, object] = {}
|
||||
|
||||
# 配色
|
||||
self.colors: Dict[str, Tuple[int, int, int]] = {
|
||||
# 配色方案
|
||||
self.colors = {
|
||||
"bg": (18, 18, 18),
|
||||
"grid": (40, 40, 40),
|
||||
"text": (230, 230, 230),
|
||||
@@ -75,30 +74,33 @@ class Front:
|
||||
}
|
||||
|
||||
# 加载tile图像
|
||||
self.tile_images: Dict[TileType, object] = {}
|
||||
self.tile_images = {}
|
||||
self._load_tile_images()
|
||||
|
||||
self.clock = pygame.time.Clock()
|
||||
|
||||
# --------------------------- 主循环 ---------------------------
|
||||
def run(self):
|
||||
"""主循环"""
|
||||
pygame = self.pygame
|
||||
running = True
|
||||
|
||||
while running:
|
||||
dt_ms = self.clock.tick(60)
|
||||
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 in (pygame.K_ESCAPE,):
|
||||
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:
|
||||
self._step_once()
|
||||
|
||||
# 自动步进
|
||||
if self._auto_step and self._last_step_ms >= self.step_interval_ms:
|
||||
self._step_once()
|
||||
|
||||
@@ -107,99 +109,115 @@ class Front:
|
||||
pygame.quit()
|
||||
|
||||
def _step_once(self):
|
||||
"""执行一步模拟"""
|
||||
self.simulator.step()
|
||||
self._last_step_ms = 0
|
||||
|
||||
# --------------------------- 渲染 ---------------------------
|
||||
def _render(self):
|
||||
"""渲染主画面"""
|
||||
pygame = self.pygame
|
||||
|
||||
# 清屏
|
||||
self.screen.fill(self.colors["bg"])
|
||||
|
||||
# 绘制地图和标签
|
||||
self._draw_map()
|
||||
hovered_region = self._draw_region_labels()
|
||||
hovered_avatar = self._draw_avatars_and_pick_hover()
|
||||
|
||||
# 优先显示region tooltip,如果没有region tooltip才显示avatar tooltip
|
||||
# 显示tooltip
|
||||
if hovered_region is not None:
|
||||
mouse_x, mouse_y = self.pygame.mouse.get_pos()
|
||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||
self._draw_tooltip_for_region(hovered_region, mouse_x, mouse_y)
|
||||
elif hovered_avatar is not None:
|
||||
self._draw_tooltip_for_avatar(hovered_avatar)
|
||||
|
||||
# 状态条
|
||||
# 状态信息
|
||||
self._draw_status_bar()
|
||||
self._draw_date_info()
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
def _draw_status_bar(self):
|
||||
"""绘制状态栏"""
|
||||
hint = f"A:自动步进({'开' if self._auto_step else '关'}) SPACE:单步 ESC:退出"
|
||||
text_surf = self.font.render(hint, True, self.colors["text"])
|
||||
self.screen.blit(text_surf, (self.margin, 4))
|
||||
|
||||
# 年月(右上角显示:YYYY年MM月)
|
||||
def _draw_date_info(self):
|
||||
"""绘制日期信息"""
|
||||
try:
|
||||
month_num = list(type(self.simulator.month)).index(self.simulator.month) + 1
|
||||
except Exception:
|
||||
month_num = 1
|
||||
|
||||
ym_text = f"{int(self.simulator.year)}年{month_num:02d}月"
|
||||
ym_surf = self.font.render(ym_text, True, self.colors["text"])
|
||||
screen_w, _ = self.screen.get_size()
|
||||
self.screen.blit(ym_surf, (screen_w - self.margin - ym_surf.get_width(), 4))
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
def _draw_map(self):
|
||||
"""绘制地图"""
|
||||
pygame = self.pygame
|
||||
map_obj = self.world.map
|
||||
ts = self.tile_size
|
||||
m = self.margin
|
||||
|
||||
# 先画tile图像
|
||||
# 绘制tile图像
|
||||
for y in range(map_obj.height):
|
||||
for x in range(map_obj.width):
|
||||
tile = map_obj.get_tile(x, y)
|
||||
tile_image = self.tile_images.get(tile.type)
|
||||
|
||||
if tile_image:
|
||||
# 使用tile图像
|
||||
pos = (m + x * ts, m + y * ts)
|
||||
self.screen.blit(tile_image, pos)
|
||||
else:
|
||||
# 如果没有图像,使用默认颜色块
|
||||
color = (80, 80, 80) # 默认灰色
|
||||
# 默认颜色块
|
||||
color = (80, 80, 80)
|
||||
rect = pygame.Rect(m + x * ts, m + y * ts, ts, ts)
|
||||
pygame.draw.rect(self.screen, color, rect)
|
||||
|
||||
# 画网格线
|
||||
# 绘制网格线
|
||||
self._draw_grid(map_obj, ts, m)
|
||||
|
||||
def _draw_grid(self, map_obj, ts, m):
|
||||
"""绘制网格线"""
|
||||
pygame = self.pygame
|
||||
grid_color = self.colors["grid"]
|
||||
|
||||
# 垂直线
|
||||
for gx in range(map_obj.width + 1):
|
||||
start_pos = (m + gx * ts, m)
|
||||
end_pos = (m + gx * ts, m + map_obj.height * ts)
|
||||
pygame.draw.line(self.screen, grid_color, start_pos, end_pos, 1)
|
||||
|
||||
# 水平线
|
||||
for gy in range(map_obj.height + 1):
|
||||
start_pos = (m, m + gy * ts)
|
||||
end_pos = (m + map_obj.width * ts, m + gy * ts)
|
||||
pygame.draw.line(self.screen, grid_color, start_pos, end_pos, 1)
|
||||
|
||||
def _draw_region_labels(self):
|
||||
"""绘制区域标签"""
|
||||
pygame = self.pygame
|
||||
map_obj = self.world.map
|
||||
ts = self.tile_size
|
||||
m = self.margin
|
||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||
|
||||
# 聚合每个 region 的所有地块中心点:Region 以自身 id 为哈希键
|
||||
region_to_points: Dict[object, List[Tuple[int, int]]] = {}
|
||||
# 直接遍历底层 tiles 字典更高效
|
||||
for (x, y), tile in getattr(map_obj, "tiles", {}).items():
|
||||
if getattr(tile, "region", None) is None:
|
||||
continue
|
||||
region_obj = tile.region
|
||||
cx = m + x * ts + ts // 2
|
||||
cy = m + y * ts + ts // 2
|
||||
region_to_points.setdefault(region_obj, []).append((cx, cy))
|
||||
|
||||
# 收集每个region的所有地块中心点
|
||||
region_to_points = self._collect_region_points(map_obj, ts, m)
|
||||
|
||||
if not region_to_points:
|
||||
return
|
||||
return None
|
||||
|
||||
# 绘制每个region的标签
|
||||
hovered_region = None
|
||||
for region, points in region_to_points.items():
|
||||
if not points:
|
||||
continue
|
||||
|
||||
# 计算质心
|
||||
avg_x = sum(p[0] for p in points) // len(points)
|
||||
avg_y = sum(p[1] for p in points) // len(points)
|
||||
@@ -208,17 +226,15 @@ class Front:
|
||||
if not name:
|
||||
continue
|
||||
|
||||
# 按区域大小与格子尺寸决定字体大小
|
||||
area = len(points)
|
||||
base = int(self.tile_size * 1.1)
|
||||
growth = int(max(0, min(24, (area ** 0.5))))
|
||||
font_size = max(16, min(40, base + growth))
|
||||
# 计算字体大小
|
||||
font_size = self._calculate_font_size(len(points))
|
||||
region_font = self._get_region_font(font_size)
|
||||
|
||||
# 渲染带轻微阴影的文字
|
||||
text_surface = region_font.render(str(name), True, self.colors["text"]) # 主文字
|
||||
shadow_surface = region_font.render(str(name), True, (0, 0, 0)) # 阴影
|
||||
# 渲染文字
|
||||
text_surface = region_font.render(str(name), True, self.colors["text"])
|
||||
shadow_surface = region_font.render(str(name), True, (0, 0, 0))
|
||||
|
||||
# 计算位置
|
||||
text_w = text_surface.get_width()
|
||||
text_h = text_surface.get_height()
|
||||
x = int(avg_x - text_w / 2)
|
||||
@@ -228,35 +244,55 @@ class Front:
|
||||
if (x <= mouse_x <= x + text_w and y <= mouse_y <= y + text_h):
|
||||
hovered_region = region
|
||||
|
||||
# 先画阴影,略微偏移
|
||||
# 绘制文字(先阴影后主文字)
|
||||
self.screen.blit(shadow_surface, (x + 1, y + 1))
|
||||
# 再画主文字
|
||||
self.screen.blit(text_surface, (x, y))
|
||||
|
||||
# 返回悬停的region
|
||||
return hovered_region
|
||||
|
||||
def _collect_region_points(self, map_obj, ts, m):
|
||||
"""收集region的点位信息"""
|
||||
region_to_points = {}
|
||||
|
||||
for (x, y), tile in getattr(map_obj, "tiles", {}).items():
|
||||
if getattr(tile, "region", None) is None:
|
||||
continue
|
||||
|
||||
region_obj = tile.region
|
||||
cx = m + x * ts + ts // 2
|
||||
cy = m + y * ts + ts // 2
|
||||
region_to_points.setdefault(region_obj, []).append((cx, cy))
|
||||
|
||||
return region_to_points
|
||||
|
||||
def _calculate_font_size(self, area):
|
||||
"""根据区域大小计算字体大小"""
|
||||
base = int(self.tile_size * 1.1)
|
||||
growth = int(max(0, min(24, (area ** 0.5))))
|
||||
return max(16, min(40, base + growth))
|
||||
|
||||
def _get_region_font(self, size: int):
|
||||
# 缓存不同大小的字体,避免每帧重复创建
|
||||
f = self._region_font_cache.get(size)
|
||||
if f is None:
|
||||
f = self._create_font(size)
|
||||
self._region_font_cache[size] = f
|
||||
return f
|
||||
"""获取指定大小的字体(带缓存)"""
|
||||
if size not in self._region_font_cache:
|
||||
self._region_font_cache[size] = self._create_font(size)
|
||||
return self._region_font_cache[size]
|
||||
|
||||
def _draw_avatars_and_pick_hover(self) -> Optional[Avatar]:
|
||||
"""绘制Avatar并检测悬停"""
|
||||
pygame = self.pygame
|
||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||
|
||||
hovered: Optional[Avatar] = None
|
||||
hovered = None
|
||||
min_dist = float("inf")
|
||||
|
||||
for avatar in self.simulator.avatars:
|
||||
cx, cy = self._avatar_center_pixel(avatar)
|
||||
radius = max(8, self.tile_size // 3)
|
||||
|
||||
# 绘制Avatar
|
||||
pygame.draw.circle(self.screen, self.colors["avatar"], (cx, cy), radius)
|
||||
|
||||
# 简单的 hover:鼠标与圆心距离
|
||||
# 检测悬停
|
||||
dist = math.hypot(mouse_x - cx, mouse_y - cy)
|
||||
if dist <= radius and dist < min_dist:
|
||||
hovered = avatar
|
||||
@@ -264,27 +300,61 @@ class Front:
|
||||
|
||||
return hovered
|
||||
|
||||
# --------------------------- 工具/辅助 ---------------------------
|
||||
def _avatar_center_pixel(self, avatar: Avatar) -> Tuple[int, int]:
|
||||
"""计算Avatar的像素中心位置"""
|
||||
ts = self.tile_size
|
||||
m = self.margin
|
||||
px = m + avatar.pos_x * ts + ts // 2
|
||||
py = m + avatar.pos_y * ts + ts // 2
|
||||
return px, py
|
||||
|
||||
def _avatar_tooltip_lines(self, avatar: Avatar) -> List[str]:
|
||||
gender = str(avatar.gender)
|
||||
def _draw_tooltip(self, lines: List[str], mouse_x: int, mouse_y: int, font):
|
||||
"""绘制通用tooltip"""
|
||||
pygame = self.pygame
|
||||
|
||||
# 计算尺寸
|
||||
padding = 6
|
||||
spacing = 2
|
||||
surf_lines = [font.render(t, True, self.colors["text"]) for t in lines]
|
||||
width = max(s.get_width() for s in surf_lines) + padding * 2
|
||||
height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1)
|
||||
|
||||
pos = f"({avatar.pos_x}, {avatar.pos_y})"
|
||||
# 计算位置
|
||||
x = mouse_x + 12
|
||||
y = mouse_y + 12
|
||||
|
||||
# 边界修正
|
||||
screen_w, screen_h = self.screen.get_size()
|
||||
if x + width > screen_w:
|
||||
x = mouse_x - width - 12
|
||||
if y + height > screen_h:
|
||||
y = mouse_y - height - 12
|
||||
|
||||
# 绘制背景
|
||||
bg_rect = pygame.Rect(x, y, width, height)
|
||||
pygame.draw.rect(self.screen, self.colors["tooltip_bg"], bg_rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, self.colors["tooltip_bd"], bg_rect, 1, border_radius=6)
|
||||
|
||||
# 绘制文字
|
||||
cursor_y = y + padding
|
||||
for s in surf_lines:
|
||||
self.screen.blit(s, (x + padding, cursor_y))
|
||||
cursor_y += s.get_height() + spacing
|
||||
|
||||
def _draw_tooltip_for_avatar(self, avatar: Avatar):
|
||||
"""绘制Avatar的tooltip"""
|
||||
lines = [
|
||||
f"{avatar.name}#{avatar.id}",
|
||||
f"性别: {gender}",
|
||||
f"性别: {avatar.gender}",
|
||||
f"年龄: {avatar.age}",
|
||||
f"位置: {pos}",
|
||||
f"境界: {str(avatar.cultivation_progress)}",
|
||||
f"灵根: {avatar.root.value}",
|
||||
f"位置: ({avatar.pos_x}, {avatar.pos_y})",
|
||||
]
|
||||
return lines
|
||||
self._draw_tooltip(lines, *self.pygame.mouse.get_pos(), self.tooltip_font)
|
||||
|
||||
def _region_tooltip_lines(self, region) -> List[str]:
|
||||
def _draw_tooltip_for_region(self, region, mouse_x: int, mouse_y: int):
|
||||
"""绘制Region的tooltip"""
|
||||
lines = [
|
||||
f"区域: {region.name}",
|
||||
f"描述: {region.description}",
|
||||
@@ -292,92 +362,23 @@ class Front:
|
||||
|
||||
# 添加灵气信息
|
||||
if hasattr(region, 'essence') and region.essence:
|
||||
# 按密度排序,显示最重要的灵气
|
||||
essence_items = []
|
||||
for essence_type, density in region.essence.density.items():
|
||||
if density > 0:
|
||||
essence_name = str(essence_type)
|
||||
essence_name = str(essence_type)
|
||||
essence_items.append((density, essence_name))
|
||||
|
||||
if essence_items:
|
||||
# 按密度降序排序
|
||||
essence_items.sort(reverse=True)
|
||||
lines.append("灵气分布:")
|
||||
for density, name in essence_items:
|
||||
# 用星号表示密度等级
|
||||
stars = "★" * density + "☆" * (10 - density)
|
||||
lines.append(f" {name}: {stars}")
|
||||
|
||||
return lines
|
||||
|
||||
def _draw_tooltip_for_avatar(self, avatar: Avatar):
|
||||
pygame = self.pygame
|
||||
lines = self._avatar_tooltip_lines(avatar)
|
||||
|
||||
# 计算尺寸
|
||||
padding = 6
|
||||
spacing = 2
|
||||
surf_lines = [self.tooltip_font.render(t, True, self.colors["text"]) for t in lines]
|
||||
width = max(s.get_width() for s in surf_lines) + padding * 2
|
||||
height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1)
|
||||
|
||||
mx, my = pygame.mouse.get_pos()
|
||||
x = mx + 12
|
||||
y = my + 12
|
||||
|
||||
# 边界修正:尽量不出屏幕
|
||||
screen_w, screen_h = self.screen.get_size()
|
||||
if x + width > screen_w:
|
||||
x = mx - width - 12
|
||||
if y + height > screen_h:
|
||||
y = my - height - 12
|
||||
|
||||
bg_rect = pygame.Rect(x, y, width, height)
|
||||
pygame.draw.rect(self.screen, self.colors["tooltip_bg"], bg_rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, self.colors["tooltip_bd"], bg_rect, 1, border_radius=6)
|
||||
|
||||
# 绘制文字
|
||||
cursor_y = y + padding
|
||||
for s in surf_lines:
|
||||
self.screen.blit(s, (x + padding, cursor_y))
|
||||
cursor_y += s.get_height() + spacing
|
||||
|
||||
def _draw_tooltip_for_region(self, region, mouse_x: int, mouse_y: int):
|
||||
pygame = self.pygame
|
||||
lines = self._region_tooltip_lines(region)
|
||||
|
||||
# 计算尺寸
|
||||
padding = 6
|
||||
spacing = 2
|
||||
surf_lines = [self.tooltip_font.render(t, True, self.colors["text"]) for t in lines]
|
||||
width = max(s.get_width() for s in surf_lines) + padding * 2
|
||||
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
|
||||
|
||||
# 边界修正:尽量不出屏幕
|
||||
screen_w, screen_h = self.screen.get_size()
|
||||
if x + width > screen_w:
|
||||
x = mouse_x - width - 12
|
||||
if y + height > screen_h:
|
||||
y = mouse_y - height - 12
|
||||
|
||||
bg_rect = pygame.Rect(x, y, width, height)
|
||||
pygame.draw.rect(self.screen, self.colors["tooltip_bg"], bg_rect, border_radius=6)
|
||||
pygame.draw.rect(self.screen, self.colors["tooltip_bd"], bg_rect, 1, border_radius=6)
|
||||
|
||||
# 绘制文字
|
||||
cursor_y = y + padding
|
||||
for s in surf_lines:
|
||||
self.screen.blit(s, (x + padding, cursor_y))
|
||||
cursor_y += s.get_height() + spacing
|
||||
|
||||
self._draw_tooltip(lines, mouse_x, mouse_y, self.tooltip_font)
|
||||
|
||||
def _load_tile_images(self):
|
||||
"""
|
||||
加载所有tile类型的图像
|
||||
"""
|
||||
"""加载所有tile类型的图像"""
|
||||
import os
|
||||
pygame = self.pygame
|
||||
|
||||
@@ -391,65 +392,55 @@ class Front:
|
||||
|
||||
for tile_type in tile_types:
|
||||
image_path = f"assets/tiles/{tile_type.value}.png"
|
||||
|
||||
if os.path.exists(image_path):
|
||||
try:
|
||||
# 加载图像并缩放到tile_size
|
||||
image = pygame.image.load(image_path)
|
||||
scaled_image = pygame.transform.scale(image, (self.tile_size, self.tile_size))
|
||||
self.tile_images[tile_type] = scaled_image
|
||||
print(f"已加载tile图像: {image_path}")
|
||||
except Exception as e:
|
||||
print(f"加载tile图像失败 {image_path}: {e}")
|
||||
# 如果加载失败,创建一个默认的颜色块
|
||||
fallback_surface = pygame.Surface((self.tile_size, self.tile_size))
|
||||
fallback_surface.fill((128, 128, 128)) # 灰色作为默认
|
||||
self.tile_images[tile_type] = fallback_surface
|
||||
self._create_fallback_surface(tile_type)
|
||||
else:
|
||||
print(f"tile图像文件不存在: {image_path}")
|
||||
# 创建默认颜色块
|
||||
fallback_surface = pygame.Surface((self.tile_size, self.tile_size))
|
||||
fallback_surface.fill((128, 128, 128))
|
||||
self.tile_images[tile_type] = fallback_surface
|
||||
self._create_fallback_surface(tile_type)
|
||||
|
||||
def _create_fallback_surface(self, tile_type):
|
||||
"""创建默认的fallback surface"""
|
||||
fallback_surface = self.pygame.Surface((self.tile_size, self.tile_size))
|
||||
fallback_surface.fill((128, 128, 128)) # 灰色
|
||||
self.tile_images[tile_type] = fallback_surface
|
||||
|
||||
def _create_font(self, size: int):
|
||||
pygame = self.pygame
|
||||
"""创建字体"""
|
||||
if self.font_path:
|
||||
try:
|
||||
return pygame.font.Font(self.font_path, size)
|
||||
return self.pygame.font.Font(self.font_path, size)
|
||||
except Exception:
|
||||
# 回退到自动匹配
|
||||
pass
|
||||
return self._load_font_with_fallback(size)
|
||||
|
||||
def _load_font_with_fallback(self, size: int):
|
||||
"""
|
||||
在不同平台上尝试加载常见等宽或中文字体,避免中文渲染为方块。
|
||||
"""
|
||||
"""加载字体,带fallback机制"""
|
||||
pygame = self.pygame
|
||||
|
||||
# 字体候选列表
|
||||
candidates = [
|
||||
# Windows 常见中文字体
|
||||
"Microsoft YaHei UI",
|
||||
"Microsoft YaHei",
|
||||
"SimHei",
|
||||
"SimSun",
|
||||
# 常见等宽/通用字体
|
||||
"Consolas",
|
||||
"DejaVu Sans",
|
||||
"DejaVu Sans Mono",
|
||||
"Arial Unicode MS",
|
||||
"Noto Sans CJK SC",
|
||||
"Noto Sans CJK",
|
||||
"Microsoft YaHei UI", "Microsoft YaHei", "SimHei", "SimSun",
|
||||
"Consolas", "DejaVu Sans", "DejaVu Sans Mono", "Arial Unicode MS",
|
||||
"Noto Sans CJK SC", "Noto Sans CJK",
|
||||
]
|
||||
|
||||
for name in candidates:
|
||||
try:
|
||||
f = pygame.font.SysFont(name, size)
|
||||
# 简单验证一下是否能渲染中文(有些字体返回成功但渲染为空)
|
||||
test = f.render("测试中文AaBb123", True, (255, 255, 255))
|
||||
font = pygame.font.SysFont(name, size)
|
||||
# 验证字体是否能渲染中文
|
||||
test = font.render("测试中文AaBb123", True, (255, 255, 255))
|
||||
if test.get_width() > 0:
|
||||
return f
|
||||
return font
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
# 退回默认字体
|
||||
return pygame.font.SysFont(None, size)
|
||||
|
||||
@@ -17,6 +17,8 @@ from src.classes.avatar import Avatar, Gender
|
||||
from src.classes.calendar import Month, Year
|
||||
from src.classes.action import Move
|
||||
from src.classes.essence import Essence, EssenceType
|
||||
from src.classes.cultivation import CultivationProgress
|
||||
from src.classes.root import Root
|
||||
|
||||
|
||||
def clamp(value: int, lo: int, hi: int) -> int:
|
||||
@@ -33,7 +35,7 @@ def circle_points(cx: int, cy: int, r: int, width: int, height: int) -> List[Tup
|
||||
return pts
|
||||
|
||||
|
||||
def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None = None) -> Map:
|
||||
def build_rich_random_map(width: int = 50, height: int = 35, *, seed: int | None = None) -> Map:
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
@@ -45,16 +47,15 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
game_map.create_tile(x, y, TileType.PLAIN)
|
||||
|
||||
# 2) 西部大漠(左侧宽带),先铺设便于后续北/南带覆盖
|
||||
desert_w = max(4, width // 5)
|
||||
desert_w = max(6, width // 6) # 增加沙漠宽度
|
||||
desert_tiles: List[Tuple[int, int]] = []
|
||||
for y in range(height):
|
||||
for x in range(0, desert_w):
|
||||
game_map.create_tile(x, y, TileType.DESERT)
|
||||
desert_tiles.append((x, y))
|
||||
# 移除绿洲,大漠里面不要有水
|
||||
|
||||
# 3) 北部雪山与冰原(顶部宽带,覆盖整宽度)
|
||||
north_band = max(3, height // 5)
|
||||
north_band = max(4, height // 6) # 增加北部带宽度
|
||||
snow_mountain_tiles: List[Tuple[int, int]] = []
|
||||
glacier_tiles: List[Tuple[int, int]] = []
|
||||
for y in range(0, north_band):
|
||||
@@ -62,17 +63,17 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
game_map.create_tile(x, y, TileType.SNOW_MOUNTAIN)
|
||||
snow_mountain_tiles.append((x, y))
|
||||
# 局部冰川簇
|
||||
for _ in range(random.randint(2, 3)):
|
||||
for _ in range(random.randint(3, 5)): # 增加冰川数量
|
||||
cx = random.randint(1, width - 2)
|
||||
cy = random.randint(0, north_band - 1)
|
||||
r = random.randint(1, 2)
|
||||
r = random.randint(1, 3) # 增加冰川半径
|
||||
for x, y in circle_points(cx, cy, r, width, height):
|
||||
if y < north_band:
|
||||
game_map.create_tile(x, y, TileType.GLACIER)
|
||||
glacier_tiles.append((x, y))
|
||||
|
||||
# 4) 南部热带雨林(底部宽带,覆盖整宽度)
|
||||
south_band = max(3, height // 5)
|
||||
south_band = max(4, height // 6) # 增加南部带宽度
|
||||
rainforest_tiles: List[Tuple[int, int]] = []
|
||||
for y in range(height - south_band, height):
|
||||
for x in range(width):
|
||||
@@ -80,7 +81,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
rainforest_tiles.append((x, y))
|
||||
|
||||
# 5) 最东海域(右侧宽带),最后铺海以覆盖前面的地形;随后在海中造岛
|
||||
sea_band_w = max(3, width // 6)
|
||||
sea_band_w = max(4, width // 7) # 增加海域宽度
|
||||
sea_x0 = width - sea_band_w
|
||||
sea_tiles: List[Tuple[int, int]] = []
|
||||
for y in range(height):
|
||||
@@ -88,82 +89,154 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
game_map.create_tile(x, y, TileType.SEA)
|
||||
sea_tiles.append((x, y))
|
||||
# 岛屿:在海域内生成若干小岛(平原/森林)
|
||||
for _ in range(random.randint(3, 5)):
|
||||
for _ in range(random.randint(4, 7)): # 增加岛屿数量
|
||||
cx = random.randint(sea_x0, width - 2)
|
||||
cy = random.randint(1, height - 2)
|
||||
r = random.randint(1, 2)
|
||||
r = random.randint(1, 3) # 增加岛屿半径
|
||||
kind = random.choice([TileType.PLAIN, TileType.FOREST])
|
||||
for x, y in circle_points(cx, cy, r, width, height):
|
||||
if x >= sea_x0:
|
||||
game_map.create_tile(x, y, kind)
|
||||
|
||||
# 6) 若干湖泊(水域圆斑,限制在中部非海域)
|
||||
for _ in range(random.randint(3, 5)):
|
||||
for _ in range(random.randint(4, 7)): # 增加湖泊数量
|
||||
cx = random.randint(max(2, desert_w + 1), sea_x0 - 2)
|
||||
cy = random.randint(north_band + 1, height - south_band - 2)
|
||||
r = random.randint(1, 3)
|
||||
r = random.randint(1, 4) # 增加湖泊半径
|
||||
for x, y in circle_points(cx, cy, r, width, height):
|
||||
if x < sea_x0:
|
||||
game_map.create_tile(x, y, TileType.WATER)
|
||||
|
||||
# 7) 中部山脉:聚集成为一堆(避开海域和上下带,左移)
|
||||
# 7) 中部山脉:聚集成为横向山脉群(避开海域和上下带,左移)
|
||||
mountain_tiles: List[Tuple[int, int]] = []
|
||||
# 左移山脉生成范围,从沙漠边缘开始,但不要延伸到太右边
|
||||
mountain_end_x = sea_x0 - max(4, width // 8) # 留出更多空间给东部
|
||||
mountain_end_x = sea_x0 - max(5, width // 10) # 留出更多空间给东部
|
||||
|
||||
# 选择一个中心点,让山脉围绕这个中心聚集
|
||||
center_x = random.randint(desert_w + 3, mountain_end_x - 3)
|
||||
center_y = random.randint(north_band + 3, height - south_band - 3)
|
||||
# 选择山脉中心区域,让山脉在这个区域内聚集
|
||||
mountain_center_x = (desert_w + mountain_end_x) // 2
|
||||
mountain_center_y = (north_band + height - south_band) // 2
|
||||
|
||||
# 生成多条山脉链,都从中心点附近开始
|
||||
for _ in range(random.randint(8, 12)):
|
||||
length = random.randint(8, 15)
|
||||
# 从中心点附近随机选择一个起始点
|
||||
start_x = center_x + random.randint(-2, 2)
|
||||
start_y = center_y + random.randint(-2, 2)
|
||||
# 生成多条横向山脉链,形成山脉群
|
||||
mountain_chains = random.randint(3, 5) # 3-5条山脉链
|
||||
for chain in range(mountain_chains):
|
||||
# 每条山脉链的起始位置在中心区域附近
|
||||
start_x = mountain_center_x + random.randint(-3, 3)
|
||||
start_y = mountain_center_y + random.randint(-2, 2)
|
||||
|
||||
# 山脉链长度
|
||||
chain_length = random.randint(12, 20) # 增加山脉长度
|
||||
|
||||
# 主要方向:横向为主,允许小幅上下摆动
|
||||
main_dx = 1 if random.random() < 0.5 else -1 # 主要横向方向
|
||||
main_dy = 0 # 主要垂直方向为0
|
||||
|
||||
x, y = start_x, start_y
|
||||
|
||||
# 随机选择方向,但倾向于向中心聚集
|
||||
directions = [(1, 0), (1, 1), (1, -1), (-1, 0), (-1, 1), (-1, -1), (0, 1), (0, -1)]
|
||||
dx, dy = random.choice(directions)
|
||||
|
||||
for _ in range(length):
|
||||
for step in range(chain_length):
|
||||
if 0 <= x < mountain_end_x and north_band <= y < height - south_band:
|
||||
game_map.create_tile(x, y, TileType.MOUNTAIN)
|
||||
mountain_tiles.append((x, y))
|
||||
# 随机改变方向,增加聚集效果
|
||||
if random.random() < 0.3:
|
||||
dx, dy = random.choice(directions)
|
||||
x += dx
|
||||
y += dy
|
||||
|
||||
# 随机添加分支山脉,增加聚集效果
|
||||
if random.random() < 0.3:
|
||||
branch_length = random.randint(2, 6)
|
||||
bx, by = x, y
|
||||
for _ in range(branch_length):
|
||||
# 分支方向:倾向于向中心聚集
|
||||
if bx < mountain_center_x:
|
||||
branch_dx = random.choice([0, 1])
|
||||
elif bx > mountain_center_x:
|
||||
branch_dx = random.choice([0, -1])
|
||||
else:
|
||||
branch_dx = random.choice([-1, 0, 1])
|
||||
|
||||
if by < mountain_center_y:
|
||||
branch_dy = random.choice([0, 1])
|
||||
elif by > mountain_center_y:
|
||||
branch_dy = random.choice([0, -1])
|
||||
else:
|
||||
branch_dy = random.choice([-1, 0, 1])
|
||||
|
||||
bx += branch_dx
|
||||
by += branch_dy
|
||||
|
||||
if (0 <= bx < mountain_end_x and north_band <= by < height - south_band and
|
||||
(bx, by) not in mountain_tiles):
|
||||
game_map.create_tile(bx, by, TileType.MOUNTAIN)
|
||||
mountain_tiles.append((bx, by))
|
||||
|
||||
# 主要方向移动
|
||||
x += main_dx
|
||||
|
||||
# 垂直方向:允许小幅摆动,但倾向于回归中心线
|
||||
if random.random() < 0.7: # 70%概率向中心回归
|
||||
if y > mountain_center_y:
|
||||
y -= 1
|
||||
elif y < mountain_center_y:
|
||||
y += 1
|
||||
else: # 30%概率随机摆动
|
||||
y += random.choice([-1, 0, 1])
|
||||
|
||||
# 确保y在有效范围内
|
||||
y = max(north_band, min(height - south_band - 1, y))
|
||||
|
||||
# 8) 中部森林:几个圆斑(调整范围与山脉一致)
|
||||
mountain_end_x = sea_x0 - max(4, width // 8) # 与山脉使用相同的结束位置
|
||||
for _ in range(random.randint(4, 7)):
|
||||
for _ in range(random.randint(5, 9)): # 增加森林数量
|
||||
cx = random.randint(desert_w + 1, mountain_end_x - 2)
|
||||
cy = random.randint(north_band + 1, height - south_band - 2)
|
||||
r = random.randint(2, 4)
|
||||
r = random.randint(2, 5) # 增加森林半径
|
||||
for x, y in circle_points(cx, cy, r, width, height):
|
||||
game_map.create_tile(x, y, TileType.FOREST)
|
||||
|
||||
# 8.5) 火山:在中央山脉附近生成一个火山
|
||||
volcano_tiles: List[Tuple[int, int]] = []
|
||||
# 在中央山脉的中心点附近生成火山
|
||||
volcano_center_x = center_x + random.randint(-1, 1)
|
||||
volcano_center_y = center_y + random.randint(-1, 1)
|
||||
volcano_radius = random.randint(2, 3)
|
||||
# 在中央山脉的边缘附近生成火山,避免覆盖重要山脉
|
||||
# 选择山脉区域的边缘位置
|
||||
volcano_edge_choices = []
|
||||
|
||||
# 检查山脉区域的四个边缘
|
||||
if mountain_center_x > desert_w + 5: # 左边缘
|
||||
volcano_edge_choices.append((mountain_center_x - 3, mountain_center_y))
|
||||
if mountain_center_x < mountain_end_x - 5: # 右边缘
|
||||
volcano_edge_choices.append((mountain_center_x + 3, mountain_center_y))
|
||||
if mountain_center_y > north_band + 5: # 上边缘
|
||||
volcano_edge_choices.append((mountain_center_x, mountain_center_y - 3))
|
||||
if mountain_center_y < height - south_band - 5: # 下边缘
|
||||
volcano_edge_choices.append((mountain_center_x, mountain_center_y + 3))
|
||||
|
||||
# 如果没有合适的边缘位置,选择山脉区域内的非山脉位置
|
||||
if not volcano_edge_choices:
|
||||
for attempt in range(10):
|
||||
vx = mountain_center_x + random.randint(-4, 4)
|
||||
vy = mountain_center_y + random.randint(-4, 4)
|
||||
if (0 <= vx < mountain_end_x and north_band <= vy < height - south_band and
|
||||
game_map.get_tile(vx, vy).type != TileType.MOUNTAIN):
|
||||
volcano_edge_choices.append((vx, vy))
|
||||
break
|
||||
|
||||
# 如果还是没有找到合适位置,就在山脉中心附近找一个
|
||||
if not volcano_edge_choices:
|
||||
volcano_edge_choices.append((mountain_center_x, mountain_center_y))
|
||||
|
||||
# 选择火山位置
|
||||
volcano_center_x, volcano_center_y = random.choice(volcano_edge_choices)
|
||||
volcano_radius = random.randint(2, 3) # 减小火山半径
|
||||
|
||||
# 生成火山,但避免覆盖重要的山脉
|
||||
for x, y in circle_points(volcano_center_x, volcano_center_y, volcano_radius, width, height):
|
||||
if 0 <= x < mountain_end_x and north_band <= y < height - south_band:
|
||||
game_map.create_tile(x, y, TileType.VOLCANO)
|
||||
volcano_tiles.append((x, y))
|
||||
if (0 <= x < mountain_end_x and north_band <= y < height - south_band):
|
||||
current_tile = game_map.get_tile(x, y)
|
||||
# 只在非山脉地形上生成火山,或者在山脉边缘生成
|
||||
if current_tile.type != TileType.MOUNTAIN or random.random() < 0.3:
|
||||
game_map.create_tile(x, y, TileType.VOLCANO)
|
||||
volcano_tiles.append((x, y))
|
||||
|
||||
# 8.6) 草原:在平原区域生成一些草原
|
||||
grassland_tiles: List[Tuple[int, int]] = []
|
||||
for _ in range(random.randint(3, 5)):
|
||||
for _ in range(random.randint(4, 7)): # 增加草原数量
|
||||
cx = random.randint(desert_w + 1, mountain_end_x - 2)
|
||||
cy = random.randint(north_band + 1, height - south_band - 2)
|
||||
r = random.randint(2, 4)
|
||||
r = random.randint(2, 5) # 增加草原半径
|
||||
for x, y in circle_points(cx, cy, r, width, height):
|
||||
if x < sea_x0:
|
||||
current_tile = game_map.get_tile(x, y)
|
||||
@@ -173,10 +246,10 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
|
||||
# 8.7) 沼泽:在水域附近生成一些沼泽
|
||||
swamp_tiles: List[Tuple[int, int]] = []
|
||||
for _ in range(random.randint(2, 4)):
|
||||
for _ in range(random.randint(3, 6)): # 增加沼泽数量
|
||||
cx = random.randint(desert_w + 1, sea_x0 - 2)
|
||||
cy = random.randint(north_band + 1, height - south_band - 2)
|
||||
r = random.randint(1, 2)
|
||||
r = random.randint(1, 3) # 增加沼泽半径
|
||||
for x, y in circle_points(cx, cy, r, width, height):
|
||||
if x < sea_x0:
|
||||
# 检查周围是否有水域
|
||||
@@ -200,7 +273,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
|
||||
# 8.8) 洞穴:在山脉附近生成一些洞穴
|
||||
cave_tiles: List[Tuple[int, int]] = []
|
||||
for _ in range(random.randint(2, 4)):
|
||||
for _ in range(random.randint(3, 6)): # 增加洞穴数量
|
||||
cx = random.randint(desert_w + 1, mountain_end_x - 1)
|
||||
cy = random.randint(north_band + 1, height - south_band - 2)
|
||||
# 检查周围是否有山脉
|
||||
@@ -224,7 +297,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
|
||||
# 8.9) 遗迹:随机在一些地方生成古代遗迹
|
||||
ruins_tiles: List[Tuple[int, int]] = []
|
||||
for _ in range(random.randint(2, 3)):
|
||||
for _ in range(random.randint(3, 5)): # 增加遗迹数量
|
||||
cx = random.randint(desert_w + 1, sea_x0 - 2)
|
||||
cy = random.randint(north_band + 1, height - south_band - 2)
|
||||
current_tile = game_map.get_tile(cx, cy)
|
||||
@@ -232,57 +305,69 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
game_map.create_tile(cx, cy, TileType.RUINS)
|
||||
ruins_tiles.append((cx, cy))
|
||||
|
||||
# 9) 城市:2~4个,2x2格子,尽量落在非极端地形
|
||||
cities = 0
|
||||
attempts = 0
|
||||
city_positions = [] # 记录城市位置用于后续生成农田
|
||||
city_tiles = [] # 记录所有城市格子
|
||||
|
||||
while cities < random.randint(2, 4) and attempts < 300: # 增加尝试次数
|
||||
attempts += 1
|
||||
# 选择城市左上角位置
|
||||
x = random.randint(0, width - 2) # 确保有2x2的空间
|
||||
y = random.randint(0, height - 2)
|
||||
|
||||
# 检查2x2区域是否都适合建城
|
||||
can_build_city = True
|
||||
for dx in range(2):
|
||||
for dy in range(2):
|
||||
nx, ny = x + dx, y + dy
|
||||
if not game_map.is_in_bounds(nx, ny):
|
||||
can_build_city = False
|
||||
break
|
||||
t = game_map.get_tile(nx, ny)
|
||||
if t.type in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.GLACIER,
|
||||
TileType.SNOW_MOUNTAIN, TileType.DESERT, TileType.VOLCANO, TileType.SWAMP,
|
||||
TileType.CAVE, TileType.RUINS):
|
||||
can_build_city = False
|
||||
break
|
||||
if not can_build_city:
|
||||
break
|
||||
|
||||
if can_build_city:
|
||||
# 创建2x2城市
|
||||
city_tiles_for_this_city = []
|
||||
for dx in range(2):
|
||||
for dy in range(2):
|
||||
nx, ny = x + dx, y + dy
|
||||
game_map.create_tile(nx, ny, TileType.CITY)
|
||||
city_tiles_for_this_city.append((nx, ny))
|
||||
city_tiles.append((nx, ny))
|
||||
city_positions.append((x, y)) # 记录左上角位置
|
||||
cities += 1
|
||||
|
||||
# 8.10) 农田:在城市附近生成一些农田
|
||||
farm_tiles: List[Tuple[int, int]] = []
|
||||
# 先收集所有城市位置
|
||||
city_positions = []
|
||||
for (tx, ty), tile in game_map.tiles.items():
|
||||
if tile.type == TileType.CITY:
|
||||
city_positions.append((tx, ty))
|
||||
|
||||
# 在每个城市周围生成农田
|
||||
for city_x, city_y in city_positions:
|
||||
for _ in range(random.randint(3, 6)):
|
||||
# 在城市周围2-4格范围内生成农田
|
||||
fx = city_x + random.randint(-4, 4)
|
||||
fy = city_y + random.randint(-4, 4)
|
||||
for _ in range(random.randint(4, 8)): # 增加农田数量
|
||||
# 在城市周围3-6格范围内生成农田
|
||||
fx = city_x + random.randint(-6, 6)
|
||||
fy = city_y + random.randint(-6, 6)
|
||||
if game_map.is_in_bounds(fx, fy):
|
||||
current_tile = game_map.get_tile(fx, fy)
|
||||
if current_tile.type in (TileType.PLAIN, TileType.GRASSLAND):
|
||||
game_map.create_tile(fx, fy, TileType.FARM)
|
||||
farm_tiles.append((fx, fy))
|
||||
|
||||
# 9) 城市:2~4个,尽量落在非极端地形
|
||||
cities = 0
|
||||
attempts = 0
|
||||
while cities < random.randint(2, 4) and attempts < 200:
|
||||
attempts += 1
|
||||
x = random.randint(0, width - 1)
|
||||
y = random.randint(0, height - 1)
|
||||
t = game_map.get_tile(x, y)
|
||||
if t.type not in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.GLACIER, TileType.SNOW_MOUNTAIN, TileType.DESERT, TileType.VOLCANO, TileType.SWAMP, TileType.CAVE, TileType.RUINS):
|
||||
game_map.create_tile(x, y, TileType.CITY)
|
||||
cities += 1
|
||||
|
||||
# 10) 创建示例 Region(演示:底色可无 region;特意设立的带名字与描述)
|
||||
if desert_tiles:
|
||||
game_map.create_region("大漠", "西部荒漠地带",
|
||||
Essence(density={EssenceType.EARTH: 8, EssenceType.FIRE: 6, EssenceType.GOLD: 4, EssenceType.WOOD: 2, EssenceType.WATER: 1}),
|
||||
desert_tiles)
|
||||
if sea_tiles:
|
||||
game_map.create_region("东海", "最东边的大海",
|
||||
Essence(density={EssenceType.WATER: 10, EssenceType.EARTH: 3, EssenceType.GOLD: 2, EssenceType.WOOD: 1, EssenceType.FIRE: 1}),
|
||||
sea_tiles)
|
||||
if rainforest_tiles:
|
||||
game_map.create_region("南疆雨林", "南部潮湿炎热的雨林",
|
||||
Essence(density={EssenceType.WOOD: 9, EssenceType.WATER: 7, EssenceType.FIRE: 5, EssenceType.EARTH: 3, EssenceType.GOLD: 2}),
|
||||
rainforest_tiles)
|
||||
|
||||
# 9.5) 生成一条横贯东西的长河(允许小幅上下摆动与随机加宽,避开沙漠和大海)
|
||||
river_tiles: List[Tuple[int, int]] = []
|
||||
# 选一条靠近中部的基准纬线,避开极北/极南带
|
||||
base_y = clamp(height // 2 + random.randint(-2, 2), north_band + 1, height - south_band - 2)
|
||||
base_y = clamp(height // 2 + random.randint(-3, 3), north_band + 2, height - south_band - 3)
|
||||
y = base_y
|
||||
|
||||
# 确保河流从西边开始,到东边结束,不断流
|
||||
for x in range(0, width):
|
||||
# 检查当前位置是否为沙漠或大海,如果是则跳过
|
||||
current_tile = game_map.get_tile(x, y)
|
||||
@@ -292,23 +377,38 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
# 开凿主河道
|
||||
game_map.create_tile(x, y, TileType.WATER)
|
||||
river_tiles.append((x, y))
|
||||
# 随机加宽 1 格(上下其一),但要避开沙漠和大海
|
||||
if random.random() < 0.45:
|
||||
wy = clamp(y + random.choice([-1, 1]), 0, height - 1)
|
||||
# 检查加宽位置是否为沙漠或大海
|
||||
wide_tile = game_map.get_tile(x, wy)
|
||||
if wide_tile.type not in (TileType.DESERT, TileType.SEA):
|
||||
game_map.create_tile(x, wy, TileType.WATER)
|
||||
river_tiles.append((x, wy))
|
||||
|
||||
# 随机加宽 1-2 格(上下其一或两个),但要避开沙漠和大海
|
||||
if random.random() < 0.6: # 增加加宽概率
|
||||
# 选择加宽方向
|
||||
wide_directions = []
|
||||
if y > 0:
|
||||
wide_tile = game_map.get_tile(x, y - 1)
|
||||
if wide_tile.type not in (TileType.DESERT, TileType.SEA):
|
||||
wide_directions.append(-1)
|
||||
if y < height - 1:
|
||||
wide_tile = game_map.get_tile(x, y + 1)
|
||||
if wide_tile.type not in (TileType.DESERT, TileType.SEA):
|
||||
wide_directions.append(1)
|
||||
|
||||
# 随机选择1-2个方向加宽
|
||||
if wide_directions:
|
||||
num_wide = random.randint(1, min(2, len(wide_directions)))
|
||||
selected_directions = random.sample(wide_directions, num_wide)
|
||||
for dy in selected_directions:
|
||||
wy = y + dy
|
||||
game_map.create_tile(x, wy, TileType.WATER)
|
||||
river_tiles.append((x, wy))
|
||||
|
||||
# 轻微摆动(-1, 0, 1),并缓慢回归基准线
|
||||
drift_choices = [-1, 0, 1]
|
||||
dy = random.choice(drift_choices)
|
||||
# 回归力:偏离过大时更倾向于向 base_y 靠拢
|
||||
if y - base_y > 2:
|
||||
dy = -1 if random.random() < 0.7 else dy
|
||||
elif base_y - y > 2:
|
||||
dy = 1 if random.random() < 0.7 else dy
|
||||
y = clamp(y + dy, 1, height - 2)
|
||||
if y - base_y > 3:
|
||||
dy = -1 if random.random() < 0.8 else dy
|
||||
elif base_y - y > 3:
|
||||
dy = 1 if random.random() < 0.8 else dy
|
||||
y = clamp(y + dy, north_band + 1, height - south_band - 2)
|
||||
|
||||
# 11) 聚类函数:用于后续命名山脉/森林
|
||||
def find_type_clusters(tile_type: TileType) -> list[list[Tuple[int, int]]]:
|
||||
@@ -528,6 +628,47 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
|
||||
"tiles": farm_tiles
|
||||
})
|
||||
|
||||
# 添加城市region
|
||||
if city_tiles:
|
||||
# 为每个城市创建单独的region
|
||||
city_names = ["长安", "洛阳", "建康", "临安", "大都", "金陵", "燕京", "成都"]
|
||||
city_name_index = 0
|
||||
|
||||
# 按城市位置分组
|
||||
city_groups = []
|
||||
used_positions = set()
|
||||
|
||||
for city_x, city_y in city_positions:
|
||||
if (city_x, city_y) not in used_positions:
|
||||
# 收集这个2x2城市的所有格子
|
||||
city_group = []
|
||||
for dx in range(2):
|
||||
for dy in range(2):
|
||||
nx, ny = city_x + dx, city_y + dy
|
||||
city_group.append((nx, ny))
|
||||
used_positions.add((nx, ny))
|
||||
city_groups.append(city_group)
|
||||
|
||||
# 为每个城市创建region
|
||||
for i, city_group in enumerate(city_groups):
|
||||
if i < len(city_names):
|
||||
city_name = city_names[i]
|
||||
else:
|
||||
city_name = f"城市{i+1}"
|
||||
|
||||
regions_cfg.append({
|
||||
"name": city_name,
|
||||
"description": f"繁华的都市,人口密集,商业繁荣",
|
||||
"essence": Essence(density={
|
||||
EssenceType.GOLD: 9, # 城市金属性灵气最高
|
||||
EssenceType.FIRE: 8, # 火属性(人气)也很高
|
||||
EssenceType.EARTH: 7,
|
||||
EssenceType.WOOD: 6,
|
||||
EssenceType.WATER: 5
|
||||
}),
|
||||
"tiles": city_group
|
||||
})
|
||||
|
||||
for i, comp in enumerate(sorted(mountain_clusters, key=len, reverse=True), start=1):
|
||||
regions_cfg.append({
|
||||
"name": f"高山{i}",
|
||||
@@ -577,6 +718,9 @@ def make_avatars(world: World, count: int = 12) -> list[Avatar]:
|
||||
birth_month = random.choice(list(Month))
|
||||
age = random.randint(16, 60)
|
||||
gender = random_gender()
|
||||
|
||||
# 随机生成level,范围从0到120(对应四个大境界)
|
||||
level = random.randint(0, 120)
|
||||
|
||||
# 找一个非海域的出生点
|
||||
for _ in range(200):
|
||||
@@ -596,8 +740,10 @@ def make_avatars(world: World, count: int = 12) -> list[Avatar]:
|
||||
birth_year=birth_year,
|
||||
age=age,
|
||||
gender=gender,
|
||||
cultivation_progress=CultivationProgress(level),
|
||||
pos_x=x,
|
||||
pos_y=y,
|
||||
root=random.choice(list(Root)), # 随机选择灵根
|
||||
)
|
||||
avatar.tile = world.map.get_tile(x, y)
|
||||
avatar.bind_action(Move)
|
||||
@@ -609,7 +755,7 @@ def main():
|
||||
# 为了每次更丰富,使用随机种子;如需复现可将 seed 固定
|
||||
# random.seed(42)
|
||||
|
||||
width, height = 36, 24
|
||||
width, height = 50, 35 # 使用新的默认尺寸
|
||||
game_map = build_rich_random_map(width=width, height=height)
|
||||
world = World(map=game_map)
|
||||
|
||||
@@ -619,7 +765,7 @@ def main():
|
||||
front = Front(
|
||||
world=world,
|
||||
simulator=sim,
|
||||
tile_size=28,
|
||||
tile_size=24, # 稍微减小tile大小以适应更大的地图
|
||||
margin=8,
|
||||
step_interval_ms=350,
|
||||
window_title="Cultivation World — Front Demo",
|
||||
|
||||
Reference in New Issue
Block a user