第 1 卷(人物主线):第 1–3 集 + 人物表

主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine
由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
This commit is contained in:
Chen Yi
2026-10-05 19:28:49 +08:00
parent 594cf48a1a
commit e0073e90bd
24 changed files with 2070 additions and 79 deletions
+81 -20
View File
@@ -14,26 +14,29 @@ from typing import Any
from defusedxml.ElementTree import iterparse
# 事件里指向其他实体的字段 → 指向哪类对象
ID_FIELDS: dict[str, str] = {
"hfid": "figure",
"hist_figure_id": "figure",
"slayer_hfid": "figure",
"slayer_item_id": "artifact",
"target_hfid": "figure",
"source_hfid": "figure",
"site_id": "site",
"site_civ_id": "entity",
"entity_id": "entity",
"attacker_civ_id": "entity",
"defender_civ_id": "entity",
"artifact_id": "artifact",
"region_id": "region",
"feature_layer_id": "region",
"deity": "figure",
"worshipper_hfid": "figure",
"creature_id": "creature",
}
# 字段分类规则来自对真实史料全部 225 种字段的穷举:
# 含 "hfid" 的字段全部指向历史人物(共 40+ 个:hfid / hfid_target / group_1_hfid /
# slayer_hfid / snatcher_hfid / seeker_hfid / wounder_hfid / teacher_hfid /
# conspirator_hfid ...);而 target_enid、identity_id、master_wcid、slayer_item_id 不是。
# 旧版手工维护的字典只列了 4 个人物字段,导致关系信息在素材里被丢掉。
_ENTITY_SUFFIXES = ("_entity_id", "_civ_id", "_enid")
_REGION_FIELDS = {"region_id", "feature_layer_id", "subregion_id"}
def kind_of(tag: str) -> str | None:
"""事件字段名 → 它指向的对象类别(非引用类字段返回 None)。"""
t = tag.lower()
if "hfid" in t or t == "deity" or t.endswith("_deity"):
return "figure"
if "artifact" in t:
return "artifact"
if t == "site_id" or t.endswith("_site_id"):
return "site"
if t in ("entity_id", "target_enid") or t.endswith(_ENTITY_SUFFIXES):
return "entity"
if t in _REGION_FIELDS:
return "region"
return None
@dataclass
@@ -81,6 +84,36 @@ class Event:
vals = self.fields.get(tag)
return vals[0] if vals else ""
def ids_of_kind(self, kind: str) -> list[int]:
"""本事件中指向该类对象的全部有效 id(去重、剔除 -1 占位)。"""
out: list[int] = []
for tag, vals in self.fields.items():
if kind_of(tag) != kind:
continue
for raw in vals:
i = _int_or_none(raw)
if i is not None and i >= 0 and i not in out:
out.append(i)
return out
def figure_ids(self) -> list[int]:
return self.ids_of_kind("figure")
def site_ids(self) -> list[int]:
return self.ids_of_kind("site")
def entity_ids(self) -> list[int]:
return self.ids_of_kind("entity")
def field_kinds(self) -> dict[str, list[int]]:
"""{类别: [id, ...]},供渲染与建图使用。"""
out: dict[str, list[int]] = {}
for kind in ("figure", "site", "entity", "artifact", "region"):
ids = self.ids_of_kind(kind)
if ids:
out[kind] = ids
return out
@dataclass
class Entity:
@@ -138,6 +171,34 @@ class World:
st = self.sites.get(sid)
return pretty(st.name) if st and st.name else f"Site#{sid}"
def render_refs(self, event: Event) -> list[str]:
"""把一条事件里的引用字段渲染成 ``tag=名字``。
保留字段名是有意的:slayer_hfid=谁 与 hfid=谁 语义完全不同,
丢掉字段名就等于丢掉了“谁对谁做了什么”。
负 id(DF 的“无此项”占位)与查不到名字的字段直接跳过。
"""
out: list[str] = []
for tag, vals in event.fields.items():
kind = kind_of(tag)
if kind is None:
continue
for raw in vals:
i = _int_or_none(raw)
if i is None or i < 0:
continue
if kind == "figure":
name = self.figure_name(i)
elif kind == "site":
name = self.site_name(i)
elif kind == "entity":
name = self.entity_name(i)
else:
name = ""
if name:
out.append(f"{tag}={name}")
return out
def event_years(self) -> tuple[int, int]:
years = [e.year for e in self.events if e.year]
return (min(years), max(years)) if years else (0, 0)