From 796f48315f5913afada7a0f36bdf47abe198dcbf Mon Sep 17 00:00:00 2001 From: bridge Date: Thu, 27 Nov 2025 21:46:42 +0800 Subject: [PATCH] refactor sect in vue --- src/classes/avatar.py | 31 +++ src/classes/region.py | 2 +- src/classes/sect.py | 78 +++++++- src/classes/sect_region.py | 2 + src/run/create_map.py | 3 + src/server/main.py | 9 +- src/sim/load/load_game.py | 18 ++ .../game/panels/info/AvatarDetail.vue | 57 ++---- .../game/panels/info/InfoPanelContainer.vue | 2 + .../game/panels/info/RegionDetail.vue | 44 ++++- .../game/panels/info/SectDetail.vue | 182 ++++++++++++++++++ .../panels/info/components/RelationRow.vue | 57 ++++++ web/src/types/core.ts | 23 +++ web/src/utils/formatters/dictionary.ts | 28 --- 14 files changed, 457 insertions(+), 79 deletions(-) create mode 100644 web/src/components/game/panels/info/SectDetail.vue create mode 100644 web/src/components/game/panels/info/components/RelationRow.vue delete mode 100644 web/src/utils/formatters/dictionary.ts diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 459bd76..011de17 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -120,6 +120,23 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): _action_cd_last_months: dict[str, int] = field(default_factory=dict) # 不缓存 effects;实时从宗门与功法合并 + def join_sect(self, sect: Sect, rank: "SectRank") -> None: + """加入宗门""" + if self.sect: + self.leave_sect() + self.sect = sect + self.sect_rank = rank + sect.add_member(self) + # 更新阵营倾向为宗门阵营(如果之前是中立或不同)- 可选,视设计而定 + # 这里暂不强制修改个人阵营,除非完全不兼容 + + def leave_sect(self) -> None: + """退出宗门""" + if self.sect: + self.sect.remove_member(self) + self.sect = None + self.sect_rank = None + def __post_init__(self): """ 在Avatar创建后自动初始化tile和HP @@ -142,6 +159,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): if self.technique is None: self.technique = get_technique_by_sect(self.sect) + # 确保宗门引用同步 + if self.sect: + self.sect.add_member(self) + # 若未设定阵营,则依据宗门/无门无派规则设置,避免后续为 None if self.alignment is None: if self.sect is not None: @@ -772,6 +793,16 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): rank_name = get_rank_display_name(self.sect_rank, self.sect) return f"{self.sect.name}{rank_name}" + def get_sect_rank_name(self) -> str: + """ + 获取宗门职位的显示名称 + """ + if self.sect is None or self.sect_rank is None: + return "散修" + + from src.classes.sect_ranks import get_rank_display_name + return get_rank_display_name(self.sect_rank, self.sect) + def set_relation(self, other: "Avatar", relation: Relation) -> None: """ 设置与另一个角色的关系。 diff --git a/src/classes/region.py b/src/classes/region.py index f754dd2..aca328b 100644 --- a/src/classes/region.py +++ b/src/classes/region.py @@ -358,7 +358,7 @@ class CultivateRegion(Region): info = super().get_structured_info() info["type_name"] = "修炼区域" info["essence"] = { - "type": self.essence_type.value, + "type": str(self.essence_type), # EssenceType.__str__ 已经返回中文名 "density": self.essence_density } return info diff --git a/src/classes/sect.py b/src/classes/sect.py index f345cba..fb67de7 100644 --- a/src/classes/sect.py +++ b/src/classes/sect.py @@ -7,6 +7,11 @@ from src.utils.df import game_configs, get_str, get_float, get_int from src.classes.effect import load_effect_from_str from src.utils.config import CONFIG +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from src.classes.avatar import Avatar + from src.classes.technique import Technique + from src.classes.sect_ranks import SectRank """ 宗门、宗门总部基础数据。 @@ -46,6 +51,25 @@ class Sect: effect_desc: str = "" # 宗门自定义职位名称(可选):SectRank -> 名称 rank_names: dict[str, str] = field(default_factory=dict) + + # 运行时成员列表:Avatar ID -> Avatar + members: dict[str, "Avatar"] = field(default_factory=dict, init=False) + # 功法对象列表:Technique + techniques: list["Technique"] = field(default_factory=list, init=False) + + def __post_init__(self): + self.members = {} + self.techniques = [] + + def add_member(self, avatar: "Avatar") -> None: + """添加成员到宗门""" + if avatar.id not in self.members: + self.members[avatar.id] = avatar + + def remove_member(self, avatar: "Avatar") -> None: + """从宗门移除成员""" + if avatar.id in self.members: + del self.members[avatar.id] def get_info(self) -> str: hq = self.headquarter @@ -73,14 +97,66 @@ class Sect: def get_structured_info(self) -> dict: hq = self.headquarter + + from src.classes.sect_ranks import RANK_ORDER + from src.server.main import resolve_avatar_pic_id + from src.classes.technique import techniques_by_name + + # 成员列表:直接从 self.members 获取 + members_list = [] + for a in self.members.values(): + rank_enum = getattr(a, "sect_rank", None) + sort_val = 999 + if rank_enum and rank_enum in RANK_ORDER: + sort_val = RANK_ORDER[rank_enum] + + members_list.append({ + "id": str(a.id), + "name": a.name, + "pic_id": resolve_avatar_pic_id(a), + "gender": a.gender.value if hasattr(a.gender, "value") else "male", + "rank": a.get_sect_rank_name(), + "realm": a.cultivation_progress.realm.value if hasattr(a, 'cultivation_progress') else "未知", + "_sort_val": sort_val + }) + # 按职位排序 + members_list.sort(key=lambda x: x["_sort_val"]) + # 清理排序字段 + for m in members_list: + del m["_sort_val"] + + # 填充 techniques + # 使用 technique_names 从全局字典中查找对应的 Technique 对象并序列化 + techniques_data = [] + for t_name in self.technique_names: + t_obj = techniques_by_name.get(t_name) + if t_obj: + techniques_data.append(t_obj.get_structured_info()) + else: + # Fallback for missing techniques: create a minimal structure + techniques_data.append({ + "name": t_name, + "desc": "(未知功法)", + "grade": "", + "color": (200, 200, 200), # Gray + "attribute": "", + "effect_desc": "" + }) + return { + "id": self.id, "name": self.name, "desc": self.desc, - "alignment": self.alignment.value, + "alignment": str(self.alignment), # 直接返回中文 "style": self.member_act_style, "hq_name": hq.name, "hq_desc": hq.desc, "effect_desc": self.effect_desc, + "techniques": techniques_data, + # 兼容旧字段,如果前端还要用的话(建议迁移后废弃) + "technique_names": self.technique_names, + "preferred_weapon": self.preferred_weapon, + "members": members_list } def _split_names(value: object) -> list[str]: diff --git a/src/classes/sect_region.py b/src/classes/sect_region.py index 25d6135..c315689 100644 --- a/src/classes/sect_region.py +++ b/src/classes/sect_region.py @@ -11,6 +11,7 @@ class SectRegion(Region): 无额外操作或属性。 """ sect_name: str + sect_id: int = -1 image_path: str | None = None def get_region_type(self) -> str: @@ -28,4 +29,5 @@ class SectRegion(Region): info = super().get_structured_info() info["type_name"] = "宗门驻地" info["sect_name"] = self.sect_name + info["sect_id"] = self.sect_id return info diff --git a/src/run/create_map.py b/src/run/create_map.py index 0175e39..5548b9c 100644 --- a/src/run/create_map.py +++ b/src/run/create_map.py @@ -133,6 +133,7 @@ def add_sect_headquarters(game_map: Map, enabled_sects: list[Sect]): north_west_cor=f"{nw_x},{nw_y}", south_east_cor=f"{se_x},{se_y}", sect_name=sect_name_for_region, + sect_id=sect.id, image_path=str(getattr(sect.headquarter, "image", None)), ) game_map.regions[region.id] = region @@ -372,6 +373,8 @@ def _scale_loaded_regions(game_map: Map) -> None: # SectRegion 透传特有字段 if hasattr(region, "sect_name"): params["sect_name"] = getattr(region, "sect_name") + if hasattr(region, "sect_id"): + params["sect_id"] = getattr(region, "sect_id") if hasattr(region, "image_path"): params["image_path"] = getattr(region, "image_path") new_region = cls(**params) # 重新构建以刷新 cors/area/center diff --git a/src/server/main.py b/src/server/main.py index 60ccf09..06e4b77 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -615,12 +615,19 @@ def get_detail_info( target = regions.get(int(target_id)) except (ValueError, TypeError): target = None + elif target_type == "sect": + try: + sid = int(target_id) + target = sects_by_id.get(sid) + except (ValueError, TypeError): + target = None if target is None: raise HTTPException(status_code=404, detail="Target not found") if hasattr(target, "get_structured_info"): - return target.get_structured_info() + info = target.get_structured_info() + return info else: # 回退到 hover info 如果没有结构化信息 if hasattr(target, "get_hover_info"): diff --git a/src/sim/load/load_game.py b/src/sim/load/load_game.py index da5c508..8c38e28 100644 --- a/src/sim/load/load_game.py +++ b/src/sim/load/load_game.py @@ -124,6 +124,24 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L # 将所有avatar添加到world world.avatar_manager.avatars = all_avatars + # 重建宗门成员关系与功法列表 + from src.classes.technique import techniques_by_name + + # 1. 重建成员 + for avatar in all_avatars.values(): + if avatar.sect: + # 存档中 avatar.sect 已经被 Avatar.from_save_dict 恢复为 Sect 对象引用 + # 但 Sect.members 是空的(因为 Sect 是重新加载配置生成的) + avatar.sect.add_member(avatar) + + # 2. 重建功法对象列表(兼容旧存档) + for sect in existed_sects: + if not sect.techniques and sect.technique_names: + sect.techniques = [] + for t_name in sect.technique_names: + if t_name in techniques_by_name: + sect.techniques.append(techniques_by_name[t_name]) + # 重建事件历史 events_data = save_data.get("events", []) for event_data in events_data: diff --git a/web/src/components/game/panels/info/AvatarDetail.vue b/web/src/components/game/panels/info/AvatarDetail.vue index 306bd93..894d6d1 100644 --- a/web/src/components/game/panels/info/AvatarDetail.vue +++ b/web/src/components/game/panels/info/AvatarDetail.vue @@ -4,6 +4,7 @@ import type { AvatarDetail, EffectEntity } from '@/types/core'; import { formatHp, formatAge } from '@/utils/formatters/number'; import StatItem from './components/StatItem.vue'; import EntityRow from './components/EntityRow.vue'; +import RelationRow from './components/RelationRow.vue'; import TagList from './components/TagList.vue'; import SecondaryPopup from './components/SecondaryPopup.vue'; import { gameApi } from '@/api/game'; @@ -30,6 +31,10 @@ function jumpToAvatar(id: string) { uiStore.select('avatar', id); } +function jumpToSect(id: string) { + uiStore.select('sect', id); +} + async function handleSetObjective() { if (!objectiveContent.value.trim()) return; try { @@ -85,7 +90,7 @@ async function handleClearObjective() { label="宗门" :value="data.sect?.name || '散修'" :sub-value="data.sect?.rank" - :on-click="data.sect ? () => showDetail(data.sect) : undefined" + :on-click="data.sect ? () => jumpToSect(data.sect.id) : undefined" />
关系
-
-
- {{ rel.name }} - {{ rel.relation }} -
-
{{ rel.sect }} · {{ rel.realm }}
-
+ />
@@ -283,42 +284,6 @@ async function handleClearObjective() { background: #1890ff; } -/* Relations */ -.relation-item { - padding: 6px 8px; - background: rgba(0, 0, 0, 0.2); - border-left: 2px solid #333; - cursor: pointer; - transition: all 0.2s; -} - -.relation-item:hover { - background: rgba(255, 255, 255, 0.05); - border-left-color: #666; -} - -.rel-head { - display: flex; - justify-content: space-between; - margin-bottom: 2px; -} - -.rel-name { - font-size: 13px; - color: #eee; - font-weight: bold; -} - -.rel-type { - font-size: 12px; - color: #aaa; -} - -.rel-sub { - font-size: 11px; - color: #666; -} - /* Modal */ .modal-overlay { position: absolute; diff --git a/web/src/components/game/panels/info/InfoPanelContainer.vue b/web/src/components/game/panels/info/InfoPanelContainer.vue index faba0b4..22e57cb 100644 --- a/web/src/components/game/panels/info/InfoPanelContainer.vue +++ b/web/src/components/game/panels/info/InfoPanelContainer.vue @@ -5,6 +5,7 @@ import { useUiStore } from '../../../../stores/ui'; // Sub-components import AvatarDetailView from './AvatarDetail.vue'; import RegionDetailView from './RegionDetail.vue'; +import SectDetailView from './SectDetail.vue'; const uiStore = useUiStore(); const panelRef = ref(null); @@ -15,6 +16,7 @@ let lastOpenAt = 0; const currentComponent = computed(() => { if (uiStore.selectedTarget?.type === 'avatar') return AvatarDetailView; if (uiStore.selectedTarget?.type === 'region') return RegionDetailView; + if (uiStore.selectedTarget?.type === 'sect') return SectDetailView; return null; }); diff --git a/web/src/components/game/panels/info/RegionDetail.vue b/web/src/components/game/panels/info/RegionDetail.vue index 0b917e4..43138df 100644 --- a/web/src/components/game/panels/info/RegionDetail.vue +++ b/web/src/components/game/panels/info/RegionDetail.vue @@ -3,12 +3,13 @@ import { ref } from 'vue'; import type { RegionDetail, EffectEntity } from '@/types/core'; import EntityRow from './components/EntityRow.vue'; import SecondaryPopup from './components/SecondaryPopup.vue'; -import { translateElement } from '@/utils/formatters/dictionary'; +import { useUiStore } from '@/stores/ui'; defineProps<{ data: RegionDetail; }>(); +const uiStore = useUiStore(); const secondaryItem = ref(null); function showDetail(item: EffectEntity | undefined) { @@ -16,6 +17,10 @@ function showDetail(item: EffectEntity | undefined) { secondaryItem.value = item; } } + +function jumpToSect(id: number) { + uiStore.select('sect', id.toString()); +}