refactor map

This commit is contained in:
bridge
2025-12-02 23:00:09 +08:00
parent ce7a1a0ca7
commit 31ab56251b
13 changed files with 270 additions and 780 deletions

View File

@@ -19,27 +19,19 @@ class Map():
# key: region.id, value: list[(x, y)]
self.region_cors: dict[int, list[tuple[int, int]]] = {}
# 加载所有region数据到Map中
self._load_regions()
def _load_regions(self):
"""从配置文件加载所有区域数据到Map实例中"""
# 延迟导入避免循环导入
from src.classes.region import regions_by_id, regions_by_name
from src.classes.region import normal_regions_by_id, normal_regions_by_name
from src.classes.region import cultivate_regions_by_id, cultivate_regions_by_name
from src.classes.region import city_regions_by_id, city_regions_by_name
# 区域字典,由外部加载器 (load_map.py) 填充
self.regions = {}
self.region_names = {}
self.sect_regions = {}
self.regions = regions_by_id
self.region_names = regions_by_name
self.normal_regions = normal_regions_by_id
self.normal_region_names = normal_regions_by_name
self.cultivate_regions = cultivate_regions_by_id
self.cultivate_region_names = cultivate_regions_by_name
self.city_regions = city_regions_by_id
self.city_region_names = city_regions_by_name
# 宗门总部区域集合(由地图生成阶段注入),保持与其他区域一致的“提前维护”策略
self.update_sect_regions()
# 这些分类字典可能暂时不再自动维护,或者需要 load_map.py 手动维护
# 为了兼容性,先初始化为空
self.normal_regions = {}
self.normal_region_names = {}
self.cultivate_regions = {}
self.cultivate_region_names = {}
self.city_regions = {}
self.city_region_names = {}
def update_sect_regions(self) -> None:
"""根据当前 self.regions 动态刷新宗门总部区域字典。"""
@@ -91,19 +83,24 @@ class Map():
def get_info(self, detailed: bool = False) -> dict:
"""
返回地图信息dict
- detailed=False各类区域返回名称列表
- detailed=True各类区域返回详细信息字符串列表
"""
# 动态分类(因为现在没有自动分类字典了)
# 或者我们简单点,不分类返回,只返回总览?
# 为了保持接口不变,我们可以现场过滤。
from src.classes.region import NormalRegion, CultivateRegion, CityRegion, SectRegion
def filter_regions(cls):
return {rid: r for rid, r in self.regions.items() if isinstance(r, cls)}
def build_regions_info(regions_dict) -> list[str]:
if detailed:
return [r.get_detailed_info() for r in regions_dict.values()]
return [r.get_info() for r in regions_dict.values()]
return {
"修炼区域(可以修炼以增进修为)": build_regions_info(self.cultivate_regions),
"普通区域(可以狩猎或采集)": build_regions_info(self.normal_regions),
"城市区域(可以交易)": build_regions_info(self.city_regions),
"宗门总部(宗门弟子可在此进行疗伤等操作)": build_regions_info(self.sect_regions),
"修炼区域(可以修炼以增进修为)": build_regions_info(filter_regions(CultivateRegion)),
"普通区域(可以狩猎或采集)": build_regions_info(filter_regions(NormalRegion)),
"城市区域(可以交易)": build_regions_info(filter_regions(CityRegion)),
"宗门总部(宗门弟子可在此进行疗伤等操作)": build_regions_info(filter_regions(SectRegion)),
}

View File

@@ -14,15 +14,10 @@ from src.classes.sect import sects_by_name
def get_tiles_from_shape(shape: 'Shape', north_west_cor: str, south_east_cor: str) -> list[tuple[int, int]]:
"""
根据形状和两个角点坐标,计算出对应的所有坐标点
Args:
shape: 区域形状
north_west_cor: 西北角坐标,格式: "x,y"
south_east_cor: 东南角坐标,格式: "x,y"
Returns:
所有坐标点的列表
"""
if not north_west_cor or not south_east_cor:
return []
nw_coords = tuple(map(int, north_west_cor.split(',')))
se_coords = tuple(map(int, south_east_cor.split(',')))
@@ -32,66 +27,46 @@ def get_tiles_from_shape(shape: 'Shape', north_west_cor: str, south_east_cor: st
coordinates = []
if shape == Shape.SQUARE or shape == Shape.RECTANGLE:
# 正方形和长方形:填充整个矩形区域
for x in range(min_x, max_x + 1):
for y in range(min_y, max_y + 1):
coordinates.append((x, y))
elif shape == Shape.MEANDERING:
# 蜿蜒形状(如河流):创建一条从西北到东南的蜿蜒路径
# 计算河流的宽度(根据距离动态调整)
distance_x = max_x - min_x
distance_y = max_y - min_y
total_distance = max(distance_x, distance_y)
# 河流宽度:距离越长,河流越宽
if total_distance < 10:
width = 1
elif total_distance < 30:
width = 2
else:
width = 3
if total_distance < 10: width = 1
elif total_distance < 30: width = 2
else: width = 3
# 生成中心路径点
path_points = []
if distance_x >= distance_y:
# 主要沿X轴方向流动
for x in range(min_x, max_x + 1):
# 计算对应的y坐标添加一些蜿蜒效果
progress = (x - min_x) / max(distance_x, 1)
base_y = min_y + int(progress * distance_y)
# 添加蜿蜒效果:使用简单的正弦波
import math
wave_amplitude = min(3, distance_y // 4) if distance_y > 0 else 0
wave_y = int(wave_amplitude * math.sin(progress * math.pi * 2))
y = max(min_y, min(max_y, base_y + wave_y))
path_points.append((x, y))
else:
# 主要沿Y轴方向流动
for y in range(min_y, max_y + 1):
progress = (y - min_y) / max(distance_y, 1)
base_x = min_x + int(progress * distance_x)
# 添加蜿蜒效果
import math
wave_amplitude = min(3, distance_x // 4) if distance_x > 0 else 0
wave_x = int(wave_amplitude * math.sin(progress * math.pi * 2))
x = max(min_x, min(max_x, base_x + wave_x))
path_points.append((x, y))
# 为每个路径点添加宽度
for px, py in path_points:
for dx in range(-width//2, width//2 + 1):
for dy in range(-width//2, width//2 + 1):
nx, ny = px + dx, py + dy
# 确保在边界内
if min_x <= nx <= max_x and min_y <= ny <= max_y:
coordinates.append((nx, ny))
# 去重并排序
return sorted(list(set(coordinates)))
@@ -99,34 +74,36 @@ def get_tiles_from_shape(shape: 'Shape', north_west_cor: str, south_east_cor: st
class Region(ABC):
"""
区域抽象基类
理想中,一些地块应当在一起组成一个区域。
比如,某山;某湖、江、海;某森林;某平原;某城市;
一些分布比如物产按照Region来分布。
再比如灵气应当也是按照region分布的。
默认一个region内部的属性是共通的。
同时NPC应当对Region有观测和认知。
"""
id: int
name: str
desc: str
shape: 'Shape'
north_west_cor: str # 西北角坐标,格式: "x,y"
south_east_cor: str # 东南角坐标,格式: "x,y"
# 这些字段将在__post_init__中设置
cors: list[tuple[int, int]] = field(init=False) # 存储所有坐标点
# 可选/默认值,因为现在主要通过外部传入 cors 初始化
shape: 'Shape' = field(default=None)
north_west_cor: str = ""
south_east_cor: str = ""
# 核心坐标数据
cors: list[tuple[int, int]] = field(default_factory=list)
# 计算字段
center_loc: tuple[int, int] = field(init=False)
area: int = field(init=False)
def __post_init__(self):
"""初始化计算字段"""
# 先计算所有坐标点
self.cors = get_tiles_from_shape(self.shape, self.north_west_cor, self.south_east_cor)
if self.shape is None:
self.shape = Shape.SQUARE # 默认值
# 如果 cors 为空且提供了旧版坐标字符串,尝试计算 (兼容性)
if not self.cors and self.north_west_cor and self.south_east_cor:
self.cors = get_tiles_from_shape(self.shape, self.north_west_cor, self.south_east_cor)
# 基于坐标点计算面积
self.area = len(self.cors)
# 计算中心位置:选取落在区域格点集合中的、最接近几何中心的点
# 计算中心位置
if self.cors:
avg_x = sum(coord[0] for coord in self.cors) // len(self.cors)
avg_y = sum(coord[1] for coord in self.cors) // len(self.cors)
@@ -138,13 +115,13 @@ class Region(ABC):
return (p[0] - avg_x) ** 2 + (p[1] - avg_y) ** 2
self.center_loc = min(self.cors, key=_dist2)
else:
# 如果没有坐标点使用边界框中心作为fallback
nw_coords = tuple(map(int, self.north_west_cor.split(',')))
se_coords = tuple(map(int, self.south_east_cor.split(',')))
self.center_loc = (
(nw_coords[0] + se_coords[0]) // 2,
(nw_coords[1] + se_coords[1]) // 2
)
# Fallback
if self.north_west_cor and self.south_east_cor:
nw = list(map(int, self.north_west_cor.split(',')))
se = list(map(int, self.south_east_cor.split(',')))
self.center_loc = ((nw[0]+se[0])//2, (nw[1]+se[1])//2)
else:
self.center_loc = (0, 0)
def __hash__(self) -> int:
return hash(self.id)
@@ -156,25 +133,18 @@ class Region(ABC):
@abstractmethod
def get_region_type(self) -> str:
"""返回区域类型的字符串表示"""
pass
def get_hover_info(self) -> list[str]:
"""
返回用于前端悬浮提示的多行信息(基础信息)。
子类可扩展更多领域信息。
"""
return [
f"区域: {self.name}",
f"描述: {self.desc}",
]
def get_info(self) -> str:
# 简版:仅返回名称
return self.name
def get_detailed_info(self) -> str:
# 基类暂无更多结构化信息,详细版返回名称+描述
return f"{self.name} - {self.desc}"
def get_structured_info(self) -> dict:
@@ -188,57 +158,33 @@ class Region(ABC):
class Shape(Enum):
"""
区域形状类型
"""
SQUARE = "square" # 正方形
RECTANGLE = "rectangle" # 长方形
MEANDERING = "meandering" # 蜿蜒的(如河流)
SQUARE = "square"
RECTANGLE = "rectangle"
MEANDERING = "meandering"
@classmethod
def from_str(cls, shape_str: str) -> 'Shape':
"""
从字符串创建Shape实例
Args:
shape_str: 形状的字符串表示,如 "square", "rectangle", "meandering"
Returns:
对应的Shape枚举值
Raises:
ValueError: 如果字符串不匹配任何已知的形状类型
"""
if not shape_str: return cls.SQUARE
for shape in cls:
if shape.value == shape_str:
return shape
raise ValueError(f"Unknown shape type: {shape_str}")
return cls.SQUARE
@dataclass
class NormalRegion(Region):
"""
普通区域 - 平原、大河之类的,没有灵气或灵气很低
包含该区域分布的动植物物种信息
"""
animal_ids: list[int] = field(default_factory=list) # 该区域分布的动物物种IDs
plant_ids: list[int] = field(default_factory=list) # 该区域分布的植物物种IDs
"""普通区域"""
animal_ids: list[int] = field(default_factory=list)
plant_ids: list[int] = field(default_factory=list)
# 这些字段将在__post_init__中设置
animals: list[Animal] = field(init=False, default_factory=list) # 该区域的动物实例
plants: list[Plant] = field(init=False, default_factory=list) # 该区域的植物实例
animals: list[Animal] = field(init=False, default_factory=list)
plants: list[Plant] = field(init=False, default_factory=list)
def __post_init__(self):
"""初始化动植物实例"""
# 先调用父类的__post_init__
super().__post_init__()
# 加载动物实例
for animal_id in self.animal_ids:
if animal_id in animals_by_id:
self.animals.append(animals_by_id[animal_id])
# 加载植物实例
for plant_id in self.plant_ids:
if plant_id in plants_by_id:
self.plants.append(plants_by_id[plant_id])
@@ -247,24 +193,14 @@ class NormalRegion(Region):
return "normal"
def get_species_info(self) -> str:
"""获取该区域动植物物种的描述信息"""
info_parts = []
if self.animals:
animal_infos = [animal.get_info() for animal in self.animals]
info_parts.extend(animal_infos)
info_parts.extend([a.get_info() for a in self.animals])
if self.plants:
plant_infos = [plant.get_info() for plant in self.plants]
info_parts.extend(plant_infos)
info_parts.extend([p.get_info() for p in self.plants])
return "; ".join(info_parts) if info_parts else "暂无特色物种"
def _get_species_brief(self) -> str:
"""
简要物种信息:仅名字与境界,用于在名称后括号展示。
例:"灵兔(练气)、青云鹿(练气)、暗影豹(筑基)"
若无物种,返回空串。
"""
briefs: list[str] = []
if self.animals:
briefs.extend([f"{a.name}{a.realm.value}" for a in self.animals])
@@ -281,7 +217,6 @@ class NormalRegion(Region):
return f"{self.name}{brief}" if brief else self.name
def get_detailed_info(self) -> str:
# 名称后追加物种简要;正文仍保留原来的详细物种描述
brief = self._get_species_brief()
name_with_brief = f"{self.name}{brief}" if brief else self.name
species_info = self.get_species_info()
@@ -302,12 +237,10 @@ class NormalRegion(Region):
@property
def is_huntable(self) -> bool:
# 如果该区域有动物,则可以狩猎
return len(self.animals) > 0
@property
def is_harvestable(self) -> bool:
# 如果该区域有植物,则可以采集
return len(self.plants) > 0
def get_structured_info(self) -> dict:
@@ -320,18 +253,13 @@ class NormalRegion(Region):
@dataclass
class CultivateRegion(Region):
"""
修炼区域 - 有灵气的区域,可以修炼
"""
essence_type: EssenceType # 最高灵气类型
essence_density: int # 最高灵气密度
essence: Essence = field(init=False) # 灵气对象,根据 essence_type 和 essence_density 生成
"""修炼区域"""
essence_type: EssenceType = EssenceType.GOLD # 默认值避免 dataclass 继承错误
essence_density: int = 0
essence: Essence = field(init=False)
def __post_init__(self):
# 先调用父类的 __post_init__
super().__post_init__()
# 创建灵气对象主要灵气类型设置为指定密度其他类型设置为0
essence_density_dict = {essence_type: 0 for essence_type in EssenceType}
essence_density_dict[self.essence_type] = self.essence_density
self.essence = Essence(essence_density_dict)
@@ -358,7 +286,7 @@ class CultivateRegion(Region):
info = super().get_structured_info()
info["type_name"] = "修炼区域"
info["essence"] = {
"type": str(self.essence_type), # EssenceType.__str__ 已经返回中文名
"type": str(self.essence_type),
"density": self.essence_density
}
return info
@@ -366,20 +294,13 @@ class CultivateRegion(Region):
@dataclass
class CityRegion(Region):
"""
城市区域 - 不能修炼,但会有特殊操作
"""
"""城市区域"""
def get_region_type(self) -> str:
return "city"
def __str__(self) -> str:
return f"城市区域:{self.name} - {self.desc}"
def get_hover_info(self) -> list[str]:
# 城市区域暂时仅展示基础信息
return super().get_hover_info()
def get_info(self) -> str:
return self.name
@@ -393,10 +314,6 @@ class CityRegion(Region):
def _normalize_region_name(name: str) -> str:
"""
将诸如 "太白金府金行灵气10" 归一化为 "太白金府"
去除常见括号及其中附加信息,并裁剪空白。
"""
s = str(name).strip()
brackets = [("(", ")"), ("", ""), ("[", "]"), ("", ""), ("", ""), ("", ""), ("<", ">"), ("", "")]
for left, right in brackets:
@@ -412,15 +329,9 @@ def _normalize_region_name(name: str) -> str:
def resolve_region(world, region: Union[Region, str]) -> Region:
"""
解析字符串或 Region 为当前 world.map 中的 Region 实例
- 字符串:先精确匹配;失败则做归一化再匹配;再做“唯一包含”匹配;最后尝试按宗门名解析宗门总部区域
- Region若 world.map.regions 中存在同 id 的实例,则返回映射后的当前实例,否则原样返回
Raises:
ValueError: 未知区域名或名称不唯一
TypeError: 不支持的类型
解析字符串或 Region 为当前 world.map 中的 Region 实例
"""
from typing import Dict # 局部导入以避免潜在循环
from typing import Dict
if isinstance(region, str):
region_name = region
@@ -438,7 +349,7 @@ def resolve_region(world, region: Union[Region, str]) -> Region:
if r2 is not None:
return r2
# 3) 唯一包含匹配(当且仅当候选唯一时)
# 3) 唯一包含匹配
candidates = [name for name in by_name.keys() if name and (name in region_name or (normalized and name in normalized))]
if len(candidates) == 1:
return by_name[candidates[0]]
@@ -451,7 +362,6 @@ def resolve_region(world, region: Union[Region, str]) -> Region:
if len(matched) == 1:
return matched[0]
# 失败:抛出明确错误提示
if candidates:
sample = ", ".join(candidates[:5])
raise ValueError(f"区域名不唯一: {region_name},候选: {sample}")
@@ -464,86 +374,3 @@ def resolve_region(world, region: Union[Region, str]) -> Region:
return region
raise TypeError(f"不支持的region类型: {type(region).__name__}")
T = TypeVar('T', NormalRegion, CultivateRegion, CityRegion)
def _load_regions(region_type: Type[T], config_name: str) -> tuple[dict[int, T], dict[str, T]]:
"""
通用的区域加载函数
Args:
region_type: 区域类型 (NormalRegion, CultivateRegion, CityRegion)
config_name: 配置文件名 ("normal_region", "cultivate_region", "city_region")
Returns:
(按ID索引的字典, 按名称索引的字典)
"""
regions_by_id: dict[int, T] = {}
regions_by_name: dict[str, T] = {}
region_df = game_configs[config_name]
for row in region_df:
# 构建基础参数
base_params = {
"id": get_int(row, "id"),
"name": get_str(row, "name"),
"desc": get_str(row, "desc"),
"shape": Shape.from_str(get_str(row, "shape")),
"north_west_cor": get_str(row, "north-west-cor"),
"south_east_cor": get_str(row, "south-east-cor")
}
# 如果是修炼区域,添加额外参数
if region_type == CultivateRegion:
base_params["essence_type"] = EssenceType.from_str(get_str(row, "root_type"))
base_params["essence_density"] = get_int(row, "root_density")
# 如果是普通区域添加动植物ID参数
elif region_type == NormalRegion:
base_params["animal_ids"] = get_list_int(row, "animal_ids")
base_params["plant_ids"] = get_list_int(row, "plant_ids")
region = region_type(**base_params)
regions_by_id[region.id] = region
regions_by_name[region.name] = region
return regions_by_id, regions_by_name
def load_all_regions() -> tuple[
dict[int, Union[NormalRegion, CultivateRegion, CityRegion]],
dict[str, Union[NormalRegion, CultivateRegion, CityRegion]]
]:
"""
统一加载所有类型的区域数据
返回: (按ID索引的字典, 按名称索引的字典)
"""
all_regions_by_id: dict[int, Union[NormalRegion, CultivateRegion, CityRegion]] = {}
all_regions_by_name: dict[str, Union[NormalRegion, CultivateRegion, CityRegion]] = {}
# 加载普通区域
normal_by_id, normal_by_name = _load_regions(NormalRegion, "normal_region")
all_regions_by_id.update(normal_by_id)
all_regions_by_name.update(normal_by_name)
# 加载修炼区域
cultivate_by_id, cultivate_by_name = _load_regions(CultivateRegion, "cultivate_region")
all_regions_by_id.update(cultivate_by_id)
all_regions_by_name.update(cultivate_by_name)
# 加载城市区域
city_by_id, city_by_name = _load_regions(CityRegion, "city_region")
all_regions_by_id.update(city_by_id)
all_regions_by_name.update(city_by_name)
return all_regions_by_id, all_regions_by_name
# 从配表加载所有区域数据
regions_by_id, regions_by_name = load_all_regions()
# 分别加载各类型区域数据
normal_regions_by_id, normal_regions_by_name = _load_regions(NormalRegion, "normal_region")
cultivate_regions_by_id, cultivate_regions_by_name = _load_regions(CultivateRegion, "cultivate_region")
city_regions_by_id, city_regions_by_name = _load_regions(CityRegion, "city_region")

View File

@@ -10,7 +10,7 @@ class SectRegion(Region):
宗门总部区域:仅用于显示宗门总部的名称与描述。
无额外操作或属性。
"""
sect_name: str
sect_name: str = ""
sect_id: int = -1
image_path: str | None = None