310 lines
10 KiB
Python
310 lines
10 KiB
Python
from dataclasses import dataclass, field
|
||
from typing import Union, TypeVar, Type, Optional, TYPE_CHECKING
|
||
from enum import Enum
|
||
from abc import ABC, abstractmethod
|
||
|
||
from src.utils.df import game_configs, get_str, get_int, get_list_int
|
||
from src.utils.config import CONFIG
|
||
from src.utils.distance import chebyshev_distance
|
||
from src.classes.essence import EssenceType, Essence
|
||
from src.classes.animal import Animal, animals_by_id
|
||
from src.classes.plant import Plant, plants_by_id
|
||
from src.classes.sect import sects_by_name
|
||
|
||
if TYPE_CHECKING:
|
||
from src.classes.avatar import Avatar
|
||
|
||
|
||
|
||
@dataclass
|
||
class Region(ABC):
|
||
"""
|
||
区域抽象基类
|
||
"""
|
||
id: int
|
||
name: str
|
||
desc: str
|
||
|
||
# 核心坐标数据,由 load_map.py 注入
|
||
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.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)
|
||
candidate = (avg_x, avg_y)
|
||
if candidate in self.cors:
|
||
self.center_loc = candidate
|
||
else:
|
||
def _dist2(p: tuple[int, int]) -> int:
|
||
return (p[0] - avg_x) ** 2 + (p[1] - avg_y) ** 2
|
||
self.center_loc = min(self.cors, key=_dist2)
|
||
else:
|
||
# Fallback
|
||
self.center_loc = (0, 0)
|
||
|
||
def __hash__(self) -> int:
|
||
return hash(self.id)
|
||
|
||
def __eq__(self, other) -> bool:
|
||
if not isinstance(other, Region):
|
||
return False
|
||
return self.id == other.id
|
||
|
||
@abstractmethod
|
||
def get_region_type(self) -> str:
|
||
pass
|
||
|
||
def get_hover_info(self) -> list[str]:
|
||
return [
|
||
f"区域: {self.name}",
|
||
f"描述: {self.desc}",
|
||
]
|
||
|
||
@abstractmethod
|
||
def _get_desc(self) -> str:
|
||
"""
|
||
返回紧跟在名字后的描述,通常包含括号,例如 '(金行灵气:5)'
|
||
注意,不需要包含self.desc
|
||
"""
|
||
pass
|
||
|
||
def _get_distance_desc(self, current_loc: tuple[int, int] = None, step_len: int = 1) -> str:
|
||
if current_loc is None:
|
||
return ""
|
||
dist = chebyshev_distance(current_loc, self.center_loc)
|
||
# 估算到达时间:距离 / 步长 (向上取整)
|
||
months = (dist + step_len - 1) // step_len
|
||
# 避免显示 0 个月
|
||
months = max(1, months)
|
||
return f"(距离:{months}月)"
|
||
|
||
def get_info(self, current_loc: tuple[int, int] = None, step_len: int = 1) -> str:
|
||
return f"{self.name}{self._get_distance_desc(current_loc, step_len)}"
|
||
|
||
def get_detailed_info(self, current_loc: tuple[int, int] = None, step_len: int = 1) -> str:
|
||
return f"{self.name}{self._get_desc()} - {self.desc}{self._get_distance_desc(current_loc, step_len)}"
|
||
|
||
def get_structured_info(self) -> dict:
|
||
return {
|
||
"id": str(self.id),
|
||
"name": self.name,
|
||
"desc": self.desc,
|
||
"type": self.get_region_type(),
|
||
"type_name": "区域"
|
||
}
|
||
|
||
|
||
@dataclass(eq=False)
|
||
class NormalRegion(Region):
|
||
"""普通区域"""
|
||
animal_ids: list[int] = field(default_factory=list)
|
||
plant_ids: list[int] = field(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):
|
||
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])
|
||
|
||
def get_region_type(self) -> str:
|
||
return "normal"
|
||
|
||
def get_species_info(self) -> str:
|
||
info_parts = []
|
||
if self.animals:
|
||
info_parts.extend([a.get_info() for a in self.animals])
|
||
if self.plants:
|
||
info_parts.extend([p.get_info() for p in self.plants])
|
||
return "; ".join(info_parts) if info_parts else "无特色物种"
|
||
|
||
def _get_desc(self) -> str:
|
||
species_info = self.get_species_info()
|
||
return f"(物种分布:{species_info})"
|
||
|
||
def __str__(self) -> str:
|
||
species_info = self.get_species_info()
|
||
return f"普通区域:{self.name} - {self.desc} | 物种分布:{species_info}"
|
||
|
||
def get_hover_info(self) -> list[str]:
|
||
lines = super().get_hover_info()
|
||
species_info = self.get_species_info()
|
||
if species_info and species_info != "暂无特色物种":
|
||
lines.append("物种分布:")
|
||
for species in species_info.split("; "):
|
||
lines.append(f" {species}")
|
||
else:
|
||
lines.append("物种分布: 暂无特色物种")
|
||
return lines
|
||
|
||
@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:
|
||
info = super().get_structured_info()
|
||
info["type_name"] = "普通区域"
|
||
|
||
# Fix: Return the actual structure instead of just calling get_structured_info on elements but never assigning
|
||
# The previous implementation (if it existed) was inherited from base or incorrect
|
||
|
||
# Assuming animals and plants are populated in __post_init__
|
||
info["animals"] = [a.get_structured_info() for a in self.animals] if self.animals else []
|
||
info["plants"] = [p.get_structured_info() for p in self.plants] if self.plants else []
|
||
|
||
return info
|
||
|
||
|
||
@dataclass(eq=False)
|
||
class CultivateRegion(Region):
|
||
"""修炼区域"""
|
||
essence_type: EssenceType = EssenceType.GOLD # 默认值避免 dataclass 继承错误
|
||
essence_density: int = 0
|
||
essence: Essence = field(init=False)
|
||
|
||
# 洞府主人:默认为空(无主)
|
||
host_avatar: Optional["Avatar"] = field(default=None, init=False)
|
||
|
||
def __post_init__(self):
|
||
super().__post_init__()
|
||
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)
|
||
|
||
def get_region_type(self) -> str:
|
||
return "cultivate"
|
||
|
||
def _get_desc(self) -> str:
|
||
return f"({self.essence_type}行灵气:{self.essence_density})"
|
||
|
||
def __str__(self) -> str:
|
||
return f"修炼区域:{self.name}({self.essence_type}行灵气:{self.essence_density})- {self.desc}"
|
||
|
||
def get_hover_info(self) -> list[str]:
|
||
lines = super().get_hover_info()
|
||
stars = "★" * self.essence_density + "☆" * (10 - self.essence_density)
|
||
lines.append(f"主要灵气: {self.essence_type} {stars}")
|
||
if self.host_avatar:
|
||
lines.append(f"主人: {self.host_avatar.name}")
|
||
else:
|
||
lines.append("主人: 无(可占据)")
|
||
return lines
|
||
|
||
def get_structured_info(self) -> dict:
|
||
info = super().get_structured_info()
|
||
info["type_name"] = "修炼区域"
|
||
info["essence"] = {
|
||
"type": str(self.essence_type),
|
||
"density": self.essence_density
|
||
}
|
||
|
||
if self.host_avatar:
|
||
info["host"] = {
|
||
"id": self.host_avatar.id,
|
||
"name": self.host_avatar.name
|
||
}
|
||
else:
|
||
info["host"] = None
|
||
|
||
return info
|
||
|
||
|
||
@dataclass(eq=False)
|
||
class CityRegion(Region):
|
||
"""城市区域"""
|
||
def get_region_type(self) -> str:
|
||
return "city"
|
||
|
||
def _get_desc(self) -> str:
|
||
return ""
|
||
|
||
def __str__(self) -> str:
|
||
return f"城市区域:{self.name} - {self.desc}"
|
||
|
||
def get_structured_info(self) -> dict:
|
||
info = super().get_structured_info()
|
||
info["type_name"] = "城市区域"
|
||
return info
|
||
|
||
|
||
def _normalize_region_name(name: str) -> str:
|
||
s = str(name).strip()
|
||
brackets = [("(", ")"), ("(", ")"), ("[", "]"), ("【", "】"), ("「", "」"), ("『", "』"), ("<", ">"), ("《", "》")]
|
||
for left, right in brackets:
|
||
while True:
|
||
start = s.find(left)
|
||
end = s.rfind(right)
|
||
if start != -1 and end != -1 and end > start:
|
||
s = (s[:start] + s[end + 1:]).strip()
|
||
else:
|
||
break
|
||
return s
|
||
|
||
|
||
def resolve_region(world, region: Union[Region, str]) -> Region:
|
||
"""
|
||
解析字符串或 Region 为当前 world.map 中的 Region 实例
|
||
"""
|
||
from typing import Dict
|
||
|
||
if isinstance(region, str):
|
||
region_name = region
|
||
by_name: Dict[str, Region] = getattr(world.map, "region_names", {})
|
||
|
||
# 1) 精确匹配
|
||
r = by_name.get(region_name)
|
||
if r is not None:
|
||
return r
|
||
|
||
# 2) 归一化后再精确匹配
|
||
normalized = _normalize_region_name(region_name)
|
||
if normalized and normalized != region_name:
|
||
r2 = by_name.get(normalized)
|
||
if r2 is not None:
|
||
return r2
|
||
|
||
# 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]]
|
||
|
||
# 4) 兜底:若传入为宗门名,则解析为其总部区域
|
||
sect = sects_by_name.get(region_name) or (sects_by_name.get(normalized) if normalized and normalized != region_name else None)
|
||
if sect is not None:
|
||
sect_regions = getattr(world.map, "sect_regions", {}) or {}
|
||
matched = [r for r in sect_regions.values() if getattr(r, "sect_name", None) == sect.name]
|
||
if len(matched) == 1:
|
||
return matched[0]
|
||
|
||
if candidates:
|
||
sample = ", ".join(candidates[:5])
|
||
raise ValueError(f"区域名不唯一: {region_name},候选: {sample}")
|
||
raise ValueError(f"未知区域名: {region_name}")
|
||
|
||
if isinstance(region, Region):
|
||
by_id = getattr(world.map, "regions", None)
|
||
if isinstance(by_id, dict) and region.id in by_id:
|
||
return by_id[region.id]
|
||
return region
|
||
|
||
raise TypeError(f"不支持的region类型: {type(region).__name__}")
|