diff --git a/assets/tiles/cave.png b/assets/tiles/cave.png index 3942d77..51af3f1 100644 Binary files a/assets/tiles/cave.png and b/assets/tiles/cave.png differ diff --git a/assets/tiles/city.png b/assets/tiles/city.png index a9fa384..e1f4227 100644 Binary files a/assets/tiles/city.png and b/assets/tiles/city.png differ diff --git a/src/front/app.py b/src/front/app.py index eb37bb9..2cd5966 100644 --- a/src/front/app.py +++ b/src/front/app.py @@ -7,7 +7,7 @@ from src.classes.avatar import Avatar, Gender from .theme import COLORS from .fonts import create_font, get_region_font as _get_region_font_cached -from .assets import load_tile_images, load_avatar_images, load_sect_images +from .assets import load_tile_images, load_avatar_images, load_sect_images, load_region_images from .rendering import ( draw_map, draw_region_labels, @@ -65,6 +65,7 @@ class Front: self.tile_images = load_tile_images(self.pygame, self.tile_size) self.sect_images = load_sect_images(self.pygame, self.tile_size) + self.region_images = load_region_images(self.pygame, self.tile_size) self.male_avatars, self.female_avatars = load_avatar_images(self.pygame, self.tile_size) self.avatar_images: Dict[str, object] = {} self._assign_avatar_images() @@ -132,8 +133,9 @@ class Front: self.margin, STATUS_BAR_HEIGHT, ) - # 底图后叠加宗门总部图层(2x2) - from .rendering import draw_sect_headquarters + # 底图后叠加小区域整图(2x2/3x3),再绘制宗门总部,避免被覆盖 + from .rendering import draw_sect_headquarters, draw_small_regions + draw_small_regions(pygame, self.screen, self.world, self.region_images, self.tile_images, self.tile_size, self.margin, STATUS_BAR_HEIGHT) 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, diff --git a/src/front/assets.py b/src/front/assets.py index c3c94de..0609c3f 100644 --- a/src/front/assets.py +++ b/src/front/assets.py @@ -70,6 +70,34 @@ def load_sect_images(pygame_mod, tile_size: int): return images -__all__ = ["load_tile_images", "load_avatar_images", "load_sect_images"] +def load_region_images(pygame_mod, tile_size: int) -> Dict[str, Dict[int, object]]: + """ + 加载小区域整图:按名称加载 assets/regions/.png。 + 为兼容 2x2 和 3x3,分别生成两种缩放版本: + - key 2 -> (tile_size*2, tile_size*2) + - key 3 -> (tile_size*3, tile_size*3) + 返回结构: { name: {2: surf2x2, 3: surf3x3} } + """ + results: Dict[str, Dict[int, object]] = {} + base_dir = Path("assets/regions") + if base_dir.exists(): + for filename in base_dir.iterdir(): + if filename.suffix.lower() != ".png" or filename.name == "original.png": + continue + try: + image = pygame_mod.image.load(str(filename)) + except pygame_mod.error: + continue + name_key = filename.stem + variants: Dict[int, object] = {} + for n in (2, 3): + w = tile_size * n + h = tile_size * n + variants[n] = pygame_mod.transform.scale(image, (w, h)) + results[name_key] = variants + return results + + +__all__ = ["load_tile_images", "load_avatar_images", "load_sect_images", "load_region_images"] diff --git a/src/front/rendering.py b/src/front/rendering.py index ee66391..e1abbbd 100644 --- a/src/front/rendering.py +++ b/src/front/rendering.py @@ -66,10 +66,72 @@ def draw_sect_headquarters(pygame_mod, screen, world, sect_images: dict, ts: int screen.blit(image, (x_px, y_px)) +def _is_small_square_region(region) -> int: + """ + 若为 2x2 或 3x3 的矩形/正方形区域,返回边长(2或3);否则返回0。 + """ + try: + nw = tuple(map(int, str(getattr(region, "north_west_cor", "0,0")).split(","))) + se = tuple(map(int, str(getattr(region, "south_east_cor", "0,0")).split(","))) + except Exception: + return 0 + if getattr(region, "shape", None) is None: + return 0 + shape_name = getattr(region.shape, "name", "") + if shape_name not in ("RECTANGLE", "SQUARE"): + return 0 + width = se[0] - nw[0] + 1 + height = se[1] - nw[1] + 1 + if width == height and width in (2, 3): + return width + return 0 + + +def draw_small_regions(pygame_mod, screen, world, region_images: dict, tile_images: dict, ts: int, m: int, top_offset: int = 0): + """ + 使用整图绘制 2x2 / 3x3 的小区域: + - 优先按名称从 region_images 中取 n×n 的整图(n 为 2 或 3) + - 若没有整图,则将现有 tile 图裁切/合成为一张,避免重复边框 + """ + for region in world.map.regions.values(): + n = _is_small_square_region(region) + if n == 0: + continue + # 仅对 2x2 生效;3x3 不覆盖(保持每格一张图) + if n != 2: + continue + try: + nw = tuple(map(int, str(getattr(region, "north_west_cor", "0,0")).split(","))) + except Exception: + continue + x_px = m + nw[0] * ts + y_px = m + top_offset + nw[1] * ts + name_key = str(getattr(region, "name", "")) + variants = region_images.get(name_key) + if variants and variants.get(n): + screen.blit(variants[n], (x_px, y_px)) + continue + # 回退:直接将该区域左上角 tile 的贴图放大为 n×n 覆盖(只用一张图,而不是四/九张) + try: + tile = world.map.get_tile(nw[0], nw[1]) + base_image = tile_images.get(tile.type) + except Exception: + base_image = None + if base_image is not None: + scaled = pygame_mod.transform.scale(base_image, (ts * n, ts * n)) + screen.blit(scaled, (x_px, y_px)) + else: + # 最后兜底:淡色块 + tmp = pygame_mod.Surface((ts * n, ts * n), pygame_mod.SRCALPHA) + tmp.fill((255, 255, 255, 24)) + screen.blit(tmp, (x_px, y_px)) + + def calculate_font_size_by_area(tile_size: int, area: int) -> int: base = int(tile_size * 1.1) growth = int(max(0, min(24, (area ** 0.5)))) - return max(16, min(40, base + growth)) + size = base + growth - 7 # 再降低2个字号 + return max(16, min(40, size)) def draw_region_labels(pygame_mod, screen, colors, world, get_region_font, tile_size: int, margin: int, top_offset: int = 0, outline_px: int = 2): @@ -101,8 +163,9 @@ def draw_region_labels(pygame_mod, screen, colors, world, get_region_font, tile_ name = getattr(region, "name", None) if not name: continue - # 以“区域最下缘的中点”为锚点(优先放在区域下方) - if getattr(region, "cors", None): + # 小区域(面积<=9,例如2x2/3x3)标签放在底部;大区域放在中心 + use_bottom = getattr(region, "area", 0) <= 9 + if use_bottom and getattr(region, "cors", None): bottom_y = max(y for _, y in region.cors) xs_on_bottom = [x for x, y in region.cors if y == bottom_y] if xs_on_bottom: @@ -111,14 +174,12 @@ def draw_region_labels(pygame_mod, screen, colors, world, get_region_font, tile_ anchor_cx_tile = (left_x + right_x) / 2.0 else: anchor_cx_tile = float(region.center_loc[0]) + screen_cx = int(m + anchor_cx_tile * ts + ts // 2) + screen_cy = int(m + top_offset + (bottom_y + 1) * ts + 2) else: - # 兜底使用中心点 - anchor_cx_tile = float(region.center_loc[0]) - bottom_y = int(region.center_loc[1]) - - screen_cx = int(m + anchor_cx_tile * ts + ts // 2) - # 锚点Y取区域底边像素的下一行,再加少量间距 - screen_cy = int(m + top_offset + (bottom_y + 1) * ts + 2) + # 居中放置 + screen_cx = int(m + float(region.center_loc[0]) * ts + ts // 2) + screen_cy = int(m + top_offset + float(region.center_loc[1]) * ts) font_size = calculate_font_size_by_area(tile_size, region.area) region_font = get_region_font(font_size) text_surface = region_font.render(str(name), True, colors["text"]) diff --git a/src/run/create_map.py b/src/run/create_map.py index ab4b28f..2e265b2 100644 --- a/src/run/create_map.py +++ b/src/run/create_map.py @@ -82,10 +82,25 @@ def add_sect_headquarters(game_map: Map, enabled_sects: list[Sect]): base_w, base_h = BASE_W, BASE_H size_w = se[0] - nw[0] size_h = se[1] - nw[1] - nw_x = max(0, min(game_map.width - 1, _scale_x(nw[0], game_map.width))) - nw_y = max(0, min(game_map.height - 1, _scale_y(nw[1], game_map.height))) - se_x = max(nw_x, min(game_map.width - 1, nw_x + size_w)) - se_y = max(nw_y, min(game_map.height - 1, nw_y + size_h)) + # 初步缩放坐标 + nw_x = _scale_x(nw[0], game_map.width) + nw_y = _scale_y(nw[1], game_map.height) + se_x = nw_x + size_w + se_y = nw_y + size_h + # 边界修正:确保 2x2 或 1x2 等固定尺寸完整在图内 + if se_x >= game_map.width: + shift = se_x - (game_map.width - 1) + nw_x -= shift + se_x -= shift + if se_y >= game_map.height: + shift = se_y - (game_map.height - 1) + nw_y -= shift + se_y -= shift + # 最终夹紧 + nw_x = max(0, min(game_map.width - 1, nw_x)) + nw_y = max(0, min(game_map.height - 1, nw_y)) + se_x = max(nw_x, min(game_map.width - 1, se_x)) + se_y = max(nw_y, min(game_map.height - 1, se_y)) region = SectRegion( id=400 + sect.id, name=hq_name, @@ -213,11 +228,11 @@ def _create_2x2_wuxing_caves(game_map: Map): """创建2*2的五行洞府区域""" # 五行洞府配置:金木水火土 wuxing_caves = [ - {"name": "太白金府", "base_x": _scale_x(26, game_map.width), "base_y": _scale_y(12, game_map.height), "element": EssenceType.GOLD, "description": "青峰山脉深处的金行洞府"}, + {"name": "太白金府", "base_x": _scale_x(24, game_map.width), "base_y": _scale_y(12, game_map.height), "element": EssenceType.GOLD, "description": "青峰山脉深处的金行洞府"}, {"name": "青木洞天", "base_x": _scale_x(48, game_map.width), "base_y": _scale_y(18, game_map.height), "element": EssenceType.WOOD, "description": "青云林海中的木行洞府"}, {"name": "玄水秘境", "base_x": _scale_x(67, game_map.width), "base_y": _scale_y(25, game_map.height), "element": EssenceType.WATER, "description": "无边碧海深处的水行洞府"}, {"name": "离火洞府", "base_x": _scale_x(48, game_map.width), "base_y": _scale_y(33, game_map.height), "element": EssenceType.FIRE, "description": "炎狱火山旁的火行洞府"}, - {"name": "厚土玄宫", "base_x": _scale_x(30, game_map.width), "base_y": _scale_y(16, game_map.height), "element": EssenceType.EARTH, "description": "青峰山脉的土行洞府"} + {"name": "厚土玄宫", "base_x": _scale_x(32, game_map.width), "base_y": _scale_y(16, game_map.height), "element": EssenceType.EARTH, "description": "青峰山脉的土行洞府"} ] for cave in wuxing_caves: @@ -281,6 +296,12 @@ def _scale_loaded_regions(game_map: Map) -> None: new_nw_y = max(0, min(height - 1, _scale_y(nw_y, height))) new_se_x = max(new_nw_x, min(width - 1, _scale_x(se_x, width))) new_se_y = max(new_nw_y, min(height - 1, _scale_y(se_y, height))) + # 夹紧到地图范围 + new_nw_x = max(0, min(width - 1, new_nw_x)) + new_se_x = max(new_nw_x, min(width - 1, new_se_x)) + new_nw_y = max(0, min(height - 1, new_nw_y)) + new_se_y = max(new_nw_y, min(height - 1, new_se_y)) + params = { "id": region.id, "name": region.name, diff --git a/src/utils/llm.py b/src/utils/llm.py index d6aa197..824c871 100644 --- a/src/utils/llm.py +++ b/src/utils/llm.py @@ -150,14 +150,9 @@ def parse_llm_response(res: str) -> dict: pass # 3) 整体 json5 兜底 - try: - obj = json5.loads(res) - if isinstance(obj, dict): - return obj - except Exception: - pass + obj = json5.loads(res) + return obj - raise ValueError("无法从LLM响应中解析出有效的JSON字典对象") def get_prompt_and_call_llm(template_path: Path, infos: dict, mode="normal") -> str: """ diff --git a/static/game_configs/cultivate_region.csv b/static/game_configs/cultivate_region.csv index 36b4015..dfc7629 100644 --- a/static/game_configs/cultivate_region.csv +++ b/static/game_configs/cultivate_region.csv @@ -1,9 +1,9 @@ id,name,desc,shape,north-west-cor,south-east-cor,root_type,root_density "ID必须以2开头",,,,,,, -201,太白金府,青峰山脉深处的金行洞府,金精气凝,刀剑鸣音不绝,乃金系修士的最高圣地。,square,"26,12","27,13",金,10 +201,太白金府,青峰山脉深处的金行洞府,金精气凝,刀剑鸣音不绝,乃金系修士的最高圣地。,square,"24,12","25,13",金,10 202,青木洞天,青云林海中的木行洞府,生机盎然,灵药遍地,乃木系修士的最高圣地。,square,"48,18","49,19",木,10 203,玄水秘境,无边碧海深处的水行洞府,碧波万里,水精凝神,乃水系修士的最高圣地。,square,"67,25","68,26",水,10 204,离火洞府,炎狱火山旁的火行洞府,烈焰冲天,真火精纯,乃火系修士的最高圣地。,square,"48,33","49,34",火,10 -205,厚土玄宫,青峰山脉的土行洞府,厚德载物,山岳共鸣,乃土系修士的最高圣地。,square,"30,16","31,17",土,10 +205,厚土玄宫,青峰山脉的土行洞府,厚德载物,山岳共鸣,乃土系修士的最高圣地。,square,"32,16","33,17",土,10 206,古越遗迹,雨林深处的上古遗迹,古藤缠绕,木行灵气与金行灵气交融。蕴藏古老功法与灵药配方。,square,"25,40","26,41",木,8 207,沧海遗迹,沉没在海中的远古文明遗迹,水行灵气浓郁,潮汐间偶有宝物现世。,square,"66,47","67,48",水,9