From 25ff7c1c17aca77bb7e970fbd53e9a0756e1ba82 Mon Sep 17 00:00:00 2001 From: bridge Date: Thu, 21 Aug 2025 00:20:26 +0800 Subject: [PATCH] add essence --- src/classes/essence.py | 40 +++++++ src/classes/tile.py | 17 +-- src/front/front.py | 80 +++++++++++++- tests/run_front.py | 236 +++++++++++++++++++++++++++++++++++------ 4 files changed, 330 insertions(+), 43 deletions(-) create mode 100644 src/classes/essence.py diff --git a/src/classes/essence.py b/src/classes/essence.py new file mode 100644 index 0000000..aad262c --- /dev/null +++ b/src/classes/essence.py @@ -0,0 +1,40 @@ +from enum import Enum + + +class EssenceType(Enum): + """ + 灵气类型 + """ + GOLD = "gold" # 金 + WOOD = "wood" # 木 + WATER = "water" # 水 + FIRE = "fire" # 火 + EARTH = "earth" # 土 + + def __str__(self) -> str: + """返回灵气类型的中文名称""" + return essence_names.get(self, self.value) + +essence_names = { + EssenceType.GOLD: "金", + EssenceType.WOOD: "木", + EssenceType.WATER: "水", + EssenceType.FIRE: "火", + EssenceType.EARTH: "土" +} + +class Essence(): + """ + 灵气,用来描述某个region的灵气情况。 + 灵气分为五种:金木水火土(先这些,之后加新的) + 每个region有五种灵气,每种灵气有不同的浓度。 + 浓度从0~10。 + """ + def __init__(self, density: dict[EssenceType, int]): + self.density = density + + def get_density(self, essence_type: EssenceType) -> int: + return self.density[essence_type] + + def set_density(self, essence_type: EssenceType, density: int): + self.density[essence_type] = density diff --git a/src/classes/tile.py b/src/classes/tile.py index cdcb298..0818445 100644 --- a/src/classes/tile.py +++ b/src/classes/tile.py @@ -1,6 +1,8 @@ +import itertools from enum import Enum from dataclasses import dataclass, field -import itertools + +from src.classes.essence import Essence, EssenceType class TileType(Enum): PLAIN = "plain" # 平原 @@ -13,6 +15,7 @@ class TileType(Enum): RAINFOREST = "rainforest" # 热带雨林 GLACIER = "glacier" # 冰川/冰原 SNOW_MOUNTAIN = "snow_mountain" # 雪山 + VOLCANO = "volcano" # 火山 region_id_counter = itertools.count(1) @@ -29,7 +32,7 @@ class Region(): """ name: str description: str - qi: int # 灵气,从0~255 + essence: Essence id: int = field(init=False) def __post_init__(self): @@ -46,13 +49,15 @@ class Region(): # 灵气 # 其他 +default_region = Region(name="平原", description="最普通的平原,没有什么可说的", essence=Essence(density={EssenceType.GOLD: 1, EssenceType.WOOD: 1, EssenceType.WATER: 1, EssenceType.FIRE: 1, EssenceType.EARTH: 1})) + @dataclass class Tile(): # 实际的地块 type: TileType x: int y: int - region: Region | None = None # 可以是一个region的一部分,也可以不属于任何region + region: Region # 可以是一个region的一部分,也可以不属于任何region class Map(): """ @@ -71,16 +76,16 @@ class Map(): return 0 <= x < self.width and 0 <= y < self.height def create_tile(self, x: int, y: int, tile_type: TileType): - self.tiles[(x, y)] = Tile(tile_type, x, y) + self.tiles[(x, y)] = Tile(tile_type, x, y, region=default_region) def get_tile(self, x: int, y: int) -> Tile: return self.tiles[(x, y)] - def create_region(self, name: str, description: str, qi: int, locs: list[tuple[int, int]]): + def create_region(self, name: str, description: str, essence: Essence, locs: list[tuple[int, int]]): """ 创建一个region。 """ - region = Region(name=name, description=description, qi=qi) + region = Region(name=name, description=description, essence=essence) for loc in locs: self.tiles[loc].region = region return region diff --git a/src/front/front.py b/src/front/front.py index 2ba9105..a579e78 100644 --- a/src/front/front.py +++ b/src/front/front.py @@ -85,6 +85,7 @@ class Front: TileType.RAINFOREST: (12, 80, 36), TileType.GLACIER: (210, 230, 240), TileType.SNOW_MOUNTAIN: (200, 200, 200), + TileType.VOLCANO: (180, 40, 40), # 火山红色 } self.clock = pygame.time.Clock() @@ -125,10 +126,15 @@ class Front: self.screen.fill(self.colors["bg"]) self._draw_map() - self._draw_region_labels() - hovered = self._draw_avatars_and_pick_hover() - if hovered is not None: - self._draw_tooltip_for_avatar(hovered) + hovered_region = self._draw_region_labels() + hovered_avatar = self._draw_avatars_and_pick_hover() + + # 优先显示region tooltip,如果没有region tooltip才显示avatar tooltip + if hovered_region is not None: + mouse_x, mouse_y = self.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) # 状态条 hint = f"A:自动步进({'开' if self._auto_step else '关'}) SPACE:单步 ESC:退出" @@ -177,6 +183,7 @@ class Front: 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]]] = {} @@ -192,6 +199,7 @@ class Front: if not region_to_points: return + hovered_region = None for region, points in region_to_points.items(): if not points: continue @@ -219,11 +227,18 @@ class Front: x = int(avg_x - text_w / 2) y = int(avg_y - text_h / 2) + # 检测鼠标悬停 + 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 _get_region_font(self, size: int): # 缓存不同大小的字体,避免每帧重复创建 f = self._region_font_cache.get(size) @@ -272,6 +287,32 @@ class Front: ] return lines + def _region_tooltip_lines(self, region) -> List[str]: + lines = [ + f"区域: {region.name}", + f"描述: {region.description}", + ] + + # 添加灵气信息 + 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_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) @@ -304,6 +345,37 @@ class Front: 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 + def _create_font(self, size: int): pygame = self.pygame diff --git a/tests/run_front.py b/tests/run_front.py index ede2d58..5694cfa 100644 --- a/tests/run_front.py +++ b/tests/run_front.py @@ -16,6 +16,7 @@ from src.classes.tile import Map, TileType 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 def clamp(value: int, lo: int, hi: int) -> int: @@ -50,20 +51,16 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None for x in range(0, desert_w): game_map.create_tile(x, y, TileType.DESERT) desert_tiles.append((x, y)) - # 绿洲 - for _ in range(random.randint(2, 3)): - cx = random.randint(1, max(1, desert_w - 1)) - cy = random.randint(2, height - 3) - r = random.randint(1, 2) - for x, y in circle_points(cx, cy, r, width, height): - if x < desert_w: - game_map.create_tile(x, y, TileType.WATER) + # 移除绿洲,大漠里面不要有水 # 3) 北部雪山与冰原(顶部宽带,覆盖整宽度) north_band = max(3, height // 5) + snow_mountain_tiles: List[Tuple[int, int]] = [] + glacier_tiles: List[Tuple[int, int]] = [] for y in range(0, north_band): for x in range(width): game_map.create_tile(x, y, TileType.SNOW_MOUNTAIN) + snow_mountain_tiles.append((x, y)) # 局部冰川簇 for _ in range(random.randint(2, 3)): cx = random.randint(1, width - 2) @@ -72,6 +69,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None 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) @@ -108,25 +106,57 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None if x < sea_x0: game_map.create_tile(x, y, TileType.WATER) - # 7) 中部山脉:多条更长的链(避开海域和上下带) - for _ in range(random.randint(6, 10)): - length = random.randint(10, 20) - x = random.randint(desert_w + 1, sea_x0 - 2) - y = random.randint(north_band + 1, height - south_band - 2) - dx, dy = random.choice([(1, 0), (1, 1), (1, -1)]) + # 7) 中部山脉:聚集成为一堆(避开海域和上下带,左移) + mountain_tiles: List[Tuple[int, int]] = [] + # 左移山脉生成范围,从沙漠边缘开始,但不要延伸到太右边 + mountain_end_x = sea_x0 - max(4, width // 8) # 留出更多空间给东部 + + # 选择一个中心点,让山脉围绕这个中心聚集 + center_x = random.randint(desert_w + 3, mountain_end_x - 3) + center_y = random.randint(north_band + 3, height - south_band - 3) + + # 生成多条山脉链,都从中心点附近开始 + 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) + 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): - if 0 <= x < sea_x0 and north_band <= y < height - south_band: + 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 - # 8) 中部森林:几个圆斑 + # 8) 中部森林:几个圆斑(调整范围与山脉一致) + mountain_end_x = sea_x0 - max(4, width // 8) # 与山脉使用相同的结束位置 for _ in range(random.randint(4, 7)): - cx = random.randint(desert_w + 1, sea_x0 - 2) + 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) 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) + + 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)) # 9) 城市:2~4个,尽量落在非极端地形 cities = 0 @@ -136,32 +166,46 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None 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): + if t.type not in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.GLACIER, TileType.SNOW_MOUNTAIN, TileType.DESERT, TileType.VOLCANO): game_map.create_tile(x, y, TileType.CITY) cities += 1 # 10) 创建示例 Region(演示:底色可无 region;特意设立的带名字与描述) if desert_tiles: - game_map.create_region("大漠", "西部荒漠地带", 40, 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("东海", "最东边的大海", 80, 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("南疆雨林", "南部潮湿炎热的雨林", 120, 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) 生成一条横贯东西的长河(允许小幅上下摆动与随机加宽) + # 9.5) 生成一条横贯东西的长河(允许小幅上下摆动与随机加宽,避开沙漠和大海) river_tiles: List[Tuple[int, int]] = [] # 选一条靠近中部的基准纬线,避开极北/极南带 base_y = clamp(height // 2 + random.randint(-2, 2), north_band + 1, height - south_band - 2) y = base_y for x in range(0, width): + # 检查当前位置是否为沙漠或大海,如果是则跳过 + current_tile = game_map.get_tile(x, y) + if current_tile.type in (TileType.DESERT, TileType.SEA): + continue + # 开凿主河道 game_map.create_tile(x, y, TileType.WATER) river_tiles.append((x, y)) - # 随机加宽 1 格(上下其一) + # 随机加宽 1 格(上下其一),但要避开沙漠和大海 if random.random() < 0.45: wy = clamp(y + random.choice([-1, 1]), 0, height - 1) - game_map.create_tile(x, wy, TileType.WATER) - river_tiles.append((x, wy)) + # 检查加宽位置是否为沙漠或大海 + 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, 0, 1),并缓慢回归基准线 drift_choices = [-1, 0, 1] dy = random.choice(drift_choices) @@ -203,22 +247,148 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None # 组装所有地理信息到一个统一的配置 dict regions_cfg: List[Dict[str, Any]] = [] if desert_tiles: - regions_cfg.append({"name": "大漠", "description": "西部荒漠地带", "qi": 40, "tiles": desert_tiles}) + regions_cfg.append({ + "name": "大漠", + "description": "西部荒漠地带", + "essence": Essence(density={ + EssenceType.EARTH: 8, + EssenceType.FIRE: 6, + EssenceType.GOLD: 4, + EssenceType.WOOD: 2, + EssenceType.WATER: 1 + }), + "tiles": desert_tiles + }) if sea_tiles: - regions_cfg.append({"name": "东海", "description": "最东边的大海", "qi": 80, "tiles": sea_tiles}) + regions_cfg.append({ + "name": "东海", + "description": "最东边的大海", + "essence": Essence(density={ + EssenceType.WATER: 10, + EssenceType.EARTH: 3, + EssenceType.GOLD: 2, + EssenceType.WOOD: 1, + EssenceType.FIRE: 1 + }), + "tiles": sea_tiles + }) if rainforest_tiles: - regions_cfg.append({"name": "南疆雨林", "description": "南部潮湿炎热的雨林", "qi": 120, "tiles": rainforest_tiles}) + regions_cfg.append({ + "name": "南疆雨林", + "description": "南部潮湿炎热的雨林", + "essence": Essence(density={ + EssenceType.WOOD: 9, + EssenceType.WATER: 7, + EssenceType.FIRE: 5, + EssenceType.EARTH: 3, + EssenceType.GOLD: 2 + }), + "tiles": rainforest_tiles + }) if river_tiles: - regions_cfg.append({"name": "大河", "description": "发源内陆,奔流入海", "qi": 100, "tiles": river_tiles}) + regions_cfg.append({ + "name": "大河", + "description": "发源内陆,奔流入海", + "essence": Essence(density={ + EssenceType.WATER: 8, + EssenceType.EARTH: 4, + EssenceType.WOOD: 3, + EssenceType.GOLD: 2, + EssenceType.FIRE: 1 + }), + "tiles": river_tiles + }) + # 添加山脉region(提高金属性灵气) + if mountain_tiles: + regions_cfg.append({ + "name": "中央山脉", + "description": "横贯大陆的中央山脉,蕴含丰富的金属性灵气", + "essence": Essence(density={ + EssenceType.GOLD: 10, # 提高金属性灵气 + EssenceType.EARTH: 9, + EssenceType.FIRE: 5, + EssenceType.WATER: 3, + EssenceType.WOOD: 2 + }), + "tiles": mountain_tiles + }) + + # 添加雪山region + if snow_mountain_tiles: + regions_cfg.append({ + "name": "北境雪山", + "description": "北部终年积雪的高山地带", + "essence": Essence(density={ + EssenceType.WATER: 9, + EssenceType.EARTH: 8, + EssenceType.GOLD: 6, + EssenceType.FIRE: 2, + EssenceType.WOOD: 1 + }), + "tiles": snow_mountain_tiles + }) + + # 添加冰川region + if glacier_tiles: + regions_cfg.append({ + "name": "极地冰川", + "description": "北部极寒的冰川地带", + "essence": Essence(density={ + EssenceType.WATER: 10, + EssenceType.EARTH: 7, + EssenceType.GOLD: 5, + EssenceType.FIRE: 1, + EssenceType.WOOD: 1 + }), + "tiles": glacier_tiles + }) + + # 添加火山region(火属性灵气最高) + if volcano_tiles: + regions_cfg.append({ + "name": "火山", + "description": "活跃的火山地带,火属性灵气极其浓郁", + "essence": Essence(density={ + EssenceType.FIRE: 10, # 火属性灵气最高 + EssenceType.EARTH: 8, + EssenceType.GOLD: 6, + EssenceType.WATER: 2, + EssenceType.WOOD: 1 + }), + "tiles": volcano_tiles + }) + for i, comp in enumerate(sorted(mountain_clusters, key=len, reverse=True), start=1): - regions_cfg.append({"name": f"高山{i}", "description": "山脉与高峰地带", "qi": 110, "tiles": comp}) + regions_cfg.append({ + "name": f"高山{i}", + "description": "山脉与高峰地带", + "essence": Essence(density={ + EssenceType.EARTH: 9, + EssenceType.GOLD: 7, + EssenceType.FIRE: 4, + EssenceType.WATER: 3, + EssenceType.WOOD: 2 + }), + "tiles": comp + }) for i, comp in enumerate(sorted(forest_clusters, key=len, reverse=True), start=1): - regions_cfg.append({"name": f"大林{i}", "description": "茂密幽深的森林", "qi": 90, "tiles": comp}) + regions_cfg.append({ + "name": f"大林{i}", + "description": "茂密幽深的森林", + "essence": Essence(density={ + EssenceType.WOOD: 8, + EssenceType.EARTH: 5, + EssenceType.WATER: 4, + EssenceType.FIRE: 2, + EssenceType.GOLD: 1 + }), + "tiles": comp + }) # 应用配置创建 Region,并把配置存到 map 上,方便前端/后续逻辑使用 for r in regions_cfg: - game_map.create_region(r["name"], r["description"], r["qi"], r["tiles"]) + game_map.create_region(r["name"], r["description"], r["essence"], r["tiles"]) geo_config: Dict[str, Any] = {"regions": regions_cfg} setattr(game_map, "geo_config", geo_config) @@ -244,7 +414,7 @@ def make_avatars(world: World, count: int = 12) -> list[Avatar]: x = random.randint(0, width - 1) y = random.randint(0, height - 1) t = world.map.get_tile(x, y) - if t.type not in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN): + if t.type not in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.VOLCANO): break else: x, y = random.randint(0, width - 1), random.randint(0, height - 1)