diff --git a/src/classes/avatar/core.py b/src/classes/avatar/core.py index 00853cc..11ccb6c 100644 --- a/src/classes/avatar/core.py +++ b/src/classes/avatar/core.py @@ -32,7 +32,7 @@ from src.classes.weapon import Weapon from src.classes.auxiliary import Auxiliary from src.classes.magic_stone import MagicStone from src.classes.hp import HP, HP_MAX_BY_REALM -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.classes.sect import Sect from src.classes.appearance import Appearance, get_random_appearance from src.classes.spirit_animal import SpiritAnimal @@ -102,6 +102,8 @@ class Avatar( materials: dict[Material, int] = field(default_factory=dict) hp: HP = field(default_factory=lambda: HP(0, 0)) relations: dict["Avatar", Relation] = field(default_factory=dict) + # 缓存的二阶关系 (由 Simulator 定期计算) + computed_relations: dict["Avatar", Relation] = field(default_factory=dict) alignment: Alignment | None = None sect: Sect | None = None sect_rank: "SectRank | None" = None @@ -392,17 +394,17 @@ class Avatar( def set_relation(self, other: "Avatar", relation: Relation) -> None: """设置与另一个角色的关系。""" - from src.classes.relations import set_relation + from src.classes.relation.relations import set_relation set_relation(self, other, relation) def get_relation(self, other: "Avatar") -> Optional[Relation]: """获取与另一个角色的关系。""" - from src.classes.relations import get_relation + from src.classes.relation.relations import get_relation return get_relation(self, other) def clear_relation(self, other: "Avatar") -> None: """清除与另一个角色的关系。""" - from src.classes.relations import clear_relation + from src.classes.relation.relations import clear_relation clear_relation(self, other) # ========== 信息展示(委托) ========== diff --git a/src/classes/avatar/info_presenter.py b/src/classes/avatar/info_presenter.py index 5ebdd30..df2ffec 100644 --- a/src/classes/avatar/info_presenter.py +++ b/src/classes/avatar/info_presenter.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from src.classes.avatar.core import Avatar from src.classes.battle import get_base_strength -from src.classes.relation import get_relation_label +from src.classes.relation.relation import get_relation_label from src.classes.emotions import EMOTION_EMOJIS, EmotionType from src.utils.config import CONFIG @@ -35,7 +35,7 @@ def get_avatar_info(avatar: "Avatar", detailed: bool = False) -> dict: """ from src.i18n import t region = avatar.tile.region if avatar.tile is not None else None - from src.classes.relation import get_relations_strs + from src.classes.relation.relation import get_relations_strs relation_lines = get_relations_strs(avatar, max_lines=8) relations_info = t("relation_separator").join(relation_lines) if relation_lines else t("None") magic_stone_info = str(avatar.magic_stone) @@ -200,6 +200,7 @@ def get_avatar_structured_info(avatar: "Avatar") -> dict: "target_id": other.id, "name": other.name, "relation": get_relation_label(relation, avatar, other), + "relation_type": relation.value, "realm": other.cultivation_progress.get_info(), "sect": other.sect.name if other.sect else t("Rogue Cultivator") }) diff --git a/src/classes/fortune.py b/src/classes/fortune.py index d3ae183..37cdced 100644 --- a/src/classes/fortune.py +++ b/src/classes/fortune.py @@ -19,7 +19,7 @@ from src.classes.technique import ( ) from src.classes.weapon import Weapon, get_random_weapon_by_realm from src.classes.auxiliary import Auxiliary, get_random_auxiliary_by_realm -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.classes.alignment import Alignment from src.classes.cultivation import Realm diff --git a/src/classes/mutual_action/conversation.py b/src/classes/mutual_action/conversation.py index 4eac75d..73c4b85 100644 --- a/src/classes/mutual_action/conversation.py +++ b/src/classes/mutual_action/conversation.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from src.i18n import t from .mutual_action import MutualAction -from src.classes.relations import ( +from src.classes.relation.relations import ( process_relation_changes, get_relation_change_context, ) diff --git a/src/classes/mutual_action/impart.py b/src/classes/mutual_action/impart.py index 3fc9300..94f2149 100644 --- a/src/classes/mutual_action/impart.py +++ b/src/classes/mutual_action/impart.py @@ -7,7 +7,7 @@ from src.i18n import t from .mutual_action import MutualAction from src.classes.action.cooldown import cooldown_action from src.classes.event import Event -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.utils.config import CONFIG if TYPE_CHECKING: diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index 8730f2b..0655759 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -9,8 +9,8 @@ from src.classes.action.action import DefineAction, ActualActionMixin, LLMAction from src.classes.event import Event from src.utils.llm import call_llm_with_task_name from src.utils.config import CONFIG -from src.classes.relation import relation_display_names, Relation -from src.classes.relations import get_possible_new_relations +from src.classes.relation.relation import Relation +from src.classes.relation.relations import get_possible_new_relations from src.classes.action_runtime import ActionResult, ActionStatus from src.classes.action.event_helper import EventHelper from src.classes.action.targeting_mixin import TargetingMixin diff --git a/src/classes/relation.py b/src/classes/relation/relation.py similarity index 68% rename from src/classes/relation.py rename to src/classes/relation/relation.py index 2b5e579..c13e9e3 100644 --- a/src/classes/relation.py +++ b/src/classes/relation/relation.py @@ -18,6 +18,16 @@ class Relation(Enum): LOVERS = "lovers" # 道侣(对称) FRIEND = "friend" # 朋友(对称) ENEMY = "enemy" # 仇人/敌人(对称) + + # —— 二阶衍生关系 (Calculated) —— + GRAND_PARENT = "grand_parent" # Parent's Parent + GRAND_CHILD = "grand_child" # Child's Child + # SIBLING 也可以是 calculated + + # 师门系 (Distance: 2) + MARTIAL_GRANDMASTER = "martial_grandmaster" # Master's Master + MARTIAL_GRANDCHILD = "martial_grandchild" # Apprentice's Apprentice + MARTIAL_SIBLING = "martial_sibling" # Shared Master def __str__(self) -> str: from src.i18n import t @@ -31,8 +41,15 @@ class Relation(Enum): if not name_cn: return None s = str(name_cn).strip() - for rel, cn in relation_display_names.items(): - if s == cn: + + # 动态查找:遍历所有关系,翻译后比对 + from src.i18n import t + for rel, msg_id in relation_msg_ids.items(): + # 这里假设当前环境语言是中文,或者我们需要强制用中文比对 + # 如果 name_cn 是 LLM 返回的中文,我们需要确保 t() 返回的是中文 + # 由于 gettext 通常基于全局上下文,这里依赖全局语言设置 + # 如果需要强制中文,可能需要临时切换 locale,但这里简化处理,假设 LLM 输出语言与当前 locale 一致 + if s == t(msg_id): return rel return None @@ -47,27 +64,25 @@ relation_msg_ids = { Relation.LOVERS: "lovers", Relation.FRIEND: "friend", Relation.ENEMY: "enemy", -} - -# 兼容性:保留旧的dict用于from_chinese方法 -relation_display_names = { - # 血缘(先天) - Relation.PARENT: "父母", - Relation.CHILD: "子女", - Relation.SIBLING: "兄弟姐妹", - Relation.KIN: "亲属", - - # 后天(社会/情感) - Relation.MASTER: "师傅", - Relation.APPRENTICE: "徒弟", - Relation.LOVERS: "道侣", - Relation.FRIEND: "朋友", - Relation.ENEMY: "仇人", + + Relation.GRAND_PARENT: "grand_parent", + Relation.GRAND_CHILD: "grand_child", + Relation.MARTIAL_GRANDMASTER: "martial_grandmaster", + Relation.MARTIAL_GRANDCHILD: "martial_grandchild", + Relation.MARTIAL_SIBLING: "martial_sibling", } # 关系是否属于“先天”(血缘),其余为“后天” INNATE_RELATIONS: set[Relation] = { Relation.PARENT, Relation.CHILD, Relation.SIBLING, Relation.KIN, + Relation.GRAND_PARENT, Relation.GRAND_CHILD +} + +# 自动计算的关系集合 +CALCULATED_RELATIONS: set[Relation] = { + Relation.GRAND_PARENT, Relation.GRAND_CHILD, + Relation.MARTIAL_GRANDMASTER, Relation.MARTIAL_GRANDCHILD, Relation.MARTIAL_SIBLING, + Relation.SIBLING } @@ -113,6 +128,8 @@ RECIPROCAL_RELATION: dict[Relation, Relation] = { Relation.CHILD: Relation.PARENT, # 子女 -> 父母 Relation.SIBLING: Relation.SIBLING, # 兄弟姐妹 -> 兄弟姐妹 Relation.KIN: Relation.KIN, # 亲属 -> 亲属 + Relation.GRAND_PARENT: Relation.GRAND_CHILD, + Relation.GRAND_CHILD: Relation.GRAND_PARENT, # 后天 Relation.MASTER: Relation.APPRENTICE, # 师傅 -> 徒弟 @@ -120,6 +137,9 @@ RECIPROCAL_RELATION: dict[Relation, Relation] = { Relation.LOVERS: Relation.LOVERS, # 道侣 -> 道侣 Relation.FRIEND: Relation.FRIEND, # 朋友 -> 朋友 Relation.ENEMY: Relation.ENEMY, # 仇人 -> 仇人 + Relation.MARTIAL_GRANDMASTER: Relation.MARTIAL_GRANDCHILD, + Relation.MARTIAL_GRANDCHILD: Relation.MARTIAL_GRANDMASTER, + Relation.MARTIAL_SIBLING: Relation.MARTIAL_SIBLING, } @@ -144,15 +164,24 @@ GENDERED_DISPLAY: dict[tuple[Relation, str], str] = { # 我 -> 对方:PARENT(我为父/母,对方为子) → 显示对方为 儿子/女儿 (Relation.PARENT, "male"): "relation_son", (Relation.PARENT, "female"): "relation_daughter", + # 祖父母 + (Relation.GRAND_PARENT, "male"): "relation_grandfather", + (Relation.GRAND_PARENT, "female"): "relation_grandmother", + # 孙辈 + (Relation.GRAND_CHILD, "male"): "relation_grandson", + (Relation.GRAND_CHILD, "female"): "relation_granddaughter", } # 显示顺序配置 DISPLAY_ORDER = [ - "master", "apprentice", "lovers", + "martial_grandmaster", "master", "martial_sibling", "apprentice", "martial_grandchild", + "lovers", + "relation_grandfather", "relation_grandmother", "grand_parent", # 祖父母 "relation_father", "relation_mother", - "relation_son", "relation_daughter", "relation_older_brother", "relation_younger_brother", "relation_older_sister", "relation_younger_sister", - "sibling", # 兜底 + "sibling", + "relation_son", "relation_daughter", + "relation_grandson", "relation_granddaughter", "grand_child", # 孙辈 "friend", "enemy", "kin" ] @@ -163,8 +192,8 @@ def get_relation_label(relation: Relation, self_avatar: "Avatar", other_avatar: """ from src.i18n import t - # 1. 处理兄弟姐妹 (涉及长幼比较) - if relation == Relation.SIBLING: + # 1. 处理兄弟姐妹/同门 (涉及长幼比较) + if relation == Relation.SIBLING or relation == Relation.MARTIAL_SIBLING: is_older = False # 比较出生时间 (MonthStamp 越小越早出生,年龄越大) if hasattr(other_avatar, "birth_month_stamp") and hasattr(self_avatar, "birth_month_stamp"): @@ -176,10 +205,18 @@ def get_relation_label(relation: Relation, self_avatar: "Avatar", other_avatar: gender_val = getattr(getattr(other_avatar, "gender", None), "value", "male") - if gender_val == "male": - return t("relation_older_brother") if is_older else t("relation_younger_brother") - else: - return t("relation_older_sister") if is_older else t("relation_younger_sister") + if relation == Relation.SIBLING: + if gender_val == "male": + return t("relation_older_brother") if is_older else t("relation_younger_brother") + else: + return t("relation_older_sister") if is_older else t("relation_younger_sister") + else: # MARTIAL_SIBLING + # 这里简单复用兄弟姐妹的 key,或者需要定义新的 key 如 martial_older_brother + # 暂时使用通用的 sibling 称谓,或者如果有专用的 key + if gender_val == "male": + return t("relation_martial_older_brother") if is_older else t("relation_martial_younger_brother") + else: + return t("relation_martial_older_sister") if is_older else t("relation_martial_younger_sister") # 2. 查表处理通用性别化称谓 other_gender = getattr(other_avatar, "gender", None) @@ -200,7 +237,11 @@ def get_relations_strs(avatar: "Avatar", max_lines: int = 12) -> list[str]: 以“我”的视角整理关系,输出若干行。 """ from src.i18n import t - relations = getattr(avatar, "relations", {}) or {} + # 融合 relations 和 computed_relations + # 优先显示一阶关系(如果同一个key在两个字典都存在,relations 覆盖 computed_relations) + # 但一般不会有重叠,除了 SIBLING 可能被提升为一阶 + relations = getattr(avatar, "computed_relations", {}).copy() + relations.update(getattr(avatar, "relations", {}) or {}) # 1. 收集并根据标签分组所有关系 grouped: dict[str, list[str]] = defaultdict(list) diff --git a/src/classes/relation_resolver.py b/src/classes/relation/relation_resolver.py similarity index 95% rename from src/classes/relation_resolver.py rename to src/classes/relation/relation_resolver.py index 8c283cc..f257c3e 100644 --- a/src/classes/relation_resolver.py +++ b/src/classes/relation/relation_resolver.py @@ -2,12 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, List, Tuple, Optional import asyncio -from src.classes.relation import ( +from src.classes.relation.relation import ( Relation, - get_relation_rules_desc, - relation_display_names + get_relation_rules_desc ) -from src.classes.relations import ( +from src.classes.relation.relations import ( set_relation, cancel_relation, ) @@ -40,7 +39,7 @@ class RelationResolver: current_rel = avatar_a.get_relation(avatar_b) rel_desc = "无" if current_rel: - rel_name = relation_display_names.get(current_rel, current_rel.value) + rel_name = str(current_rel) rel_desc = f"{rel_name}" # 获取当前世界时间 @@ -85,7 +84,7 @@ class RelationResolver: except KeyError: return None - display_name = relation_display_names.get(rel, rel_name) + display_name = str(rel) event = None if c_type == "ADD": diff --git a/src/classes/relations.py b/src/classes/relation/relations.py similarity index 70% rename from src/classes/relations.py rename to src/classes/relation/relations.py index 4325534..737d6c2 100644 --- a/src/classes/relations.py +++ b/src/classes/relation/relations.py @@ -5,7 +5,13 @@ from __future__ import annotations from typing import TYPE_CHECKING, List -from src.classes.relation import Relation, INNATE_RELATIONS, get_reciprocal, is_innate, relation_display_names +from src.classes.relation.relation import ( + Relation, + INNATE_RELATIONS, + get_reciprocal, + is_innate, + CALCULATED_RELATIONS +) from src.classes.event import Event from src.classes.action.event_helper import EventHelper @@ -13,6 +19,71 @@ if TYPE_CHECKING: from src.classes.avatar import Avatar +def update_second_degree_relations(avatar: "Avatar") -> None: + """ + 计算并更新角色的二阶关系缓存。 + 覆盖 SIBLING, GRAND_PARENT, MARTIAL_SIBLING 等。 + """ + computed = {} + + # 1. 预筛选一阶关键人 (中间节点) + relations = getattr(avatar, "relations", {}) + + parents = [t for t, r in relations.items() if r == Relation.PARENT] + children = [t for t, r in relations.items() if r == Relation.CHILD] + masters = [t for t, r in relations.items() if r == Relation.MASTER] + apprentices = [t for t, r in relations.items() if r == Relation.APPRENTICE] + + # 2. 血缘推导 + # Sibling: 父母的子女 (排除自己) + for p in parents: + # 注意:这里需要访问 parent 的 relations + # 如果 parent 是死者或者未完全加载,需要确保其 relations 可用 + p_relations = getattr(p, "relations", {}) + for sib, r in p_relations.items(): + if r == Relation.CHILD and sib.id != avatar.id: + computed[sib] = Relation.SIBLING + + # Grandparent: 父母的父母 + for p in parents: + p_relations = getattr(p, "relations", {}) + for gp, r in p_relations.items(): + if r == Relation.PARENT: + computed[gp] = Relation.GRAND_PARENT + + # Grandchild: 子女的子女 + for c in children: + c_relations = getattr(c, "relations", {}) + for gc, r in c_relations.items(): + if r == Relation.CHILD: + computed[gc] = Relation.GRAND_CHILD + + # 3. 师门推导 + # Martial Sibling: 师傅的徒弟 (排除自己) + for m in masters: + m_relations = getattr(m, "relations", {}) + for fellow, r in m_relations.items(): + if r == Relation.APPRENTICE and fellow.id != avatar.id: + computed[fellow] = Relation.MARTIAL_SIBLING + + # Martial Grandmaster: 师傅的师傅 + for m in masters: + m_relations = getattr(m, "relations", {}) + for mgm, r in m_relations.items(): + if r == Relation.MASTER: + computed[mgm] = Relation.MARTIAL_GRANDMASTER + + # Martial Grandchild: 徒弟的徒弟 + for app in apprentices: + app_relations = getattr(app, "relations", {}) + for mgc, r in app_relations.items(): + if r == Relation.APPRENTICE: + computed[mgc] = Relation.MARTIAL_GRANDCHILD + + # 4. 更新缓存 + avatar.computed_relations = computed + + def get_possible_new_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> List[Relation]: """ 评估"to_avatar 相对于 from_avatar"可能新增的后天关系集合(方向性明确)。 @@ -138,8 +209,8 @@ def get_relation_change_context(avatar1: "Avatar", avatar2: "Avatar") -> tuple[l new_rels = get_possible_new_relations(avatar1, avatar2) cancel_rels = get_possible_cancel_relations(avatar1, avatar2) - new_strs = [relation_display_names[r] for r in new_rels] - cancel_strs = [relation_display_names[r] for r in cancel_rels] + new_strs = [str(r) for r in new_rels] + cancel_strs = [str(r) for r in cancel_rels] return new_strs, cancel_strs @@ -162,7 +233,7 @@ def process_relation_changes(initiator: "Avatar", target: "Avatar", result_dict: set_relation(target, initiator, rel) set_event = Event( month_stamp, - f"{target.name} 与 {initiator.name} 的关系变为:{relation_display_names.get(rel, str(rel))}", + f"{target.name} 与 {initiator.name} 的关系变为:{str(rel)}", related_avatars=[initiator.id, target.id], is_major=True ) @@ -176,7 +247,7 @@ def process_relation_changes(initiator: "Avatar", target: "Avatar", result_dict: if success: cancel_event = Event( month_stamp, - f"{target.name} 与 {initiator.name} 取消了关系:{relation_display_names.get(rel, str(rel))}", + f"{target.name} 与 {initiator.name} 取消了关系:{str(rel)}", related_avatars=[initiator.id, target.id], is_major=True ) diff --git a/src/classes/story_teller.py b/src/classes/story_teller.py index d9039e1..f74e589 100644 --- a/src/classes/story_teller.py +++ b/src/classes/story_teller.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: from src.utils.config import CONFIG from src.utils.llm import call_llm_with_task_name -from src.classes.relations import ( +from src.classes.relation.relations import ( process_relation_changes, get_relation_change_context ) diff --git a/src/classes/tribulation.py b/src/classes/tribulation.py index 4d011ed..981301e 100644 --- a/src/classes/tribulation.py +++ b/src/classes/tribulation.py @@ -10,7 +10,7 @@ from dataclasses import dataclass if TYPE_CHECKING: from src.classes.avatar import Avatar -from src.classes.relation import Relation +from src.classes.relation.relation import Relation @dataclass diff --git a/src/i18n/locales/en_US/LC_MESSAGES/messages.mo b/src/i18n/locales/en_US/LC_MESSAGES/messages.mo index 0df47fd..1519ac0 100644 Binary files a/src/i18n/locales/en_US/LC_MESSAGES/messages.mo and b/src/i18n/locales/en_US/LC_MESSAGES/messages.mo differ diff --git a/src/i18n/locales/en_US/LC_MESSAGES/messages.po b/src/i18n/locales/en_US/LC_MESSAGES/messages.po index b372135..ec4dd81 100644 --- a/src/i18n/locales/en_US/LC_MESSAGES/messages.po +++ b/src/i18n/locales/en_US/LC_MESSAGES/messages.po @@ -3540,3 +3540,42 @@ msgstr "Cold and concise: Mainly short sentences, every word counts, like metal msgid "Fine line drawing: No decoration, capturing subtle movements and expressions to convey spirit, real and delicate." msgstr "Fine line drawing: No decoration, capturing subtle movements and expressions to convey spirit, real and delicate." + +msgid "grand_parent" +msgstr "Grandparent" + +msgid "grand_child" +msgstr "Grandchild" + +msgid "martial_grandmaster" +msgstr "Martial Grandmaster" + +msgid "martial_grandchild" +msgstr "Martial Grandchild" + +msgid "martial_sibling" +msgstr "Martial Sibling" + +msgid "relation_grandfather" +msgstr "Grandfather" + +msgid "relation_grandmother" +msgstr "Grandmother" + +msgid "relation_grandson" +msgstr "Grandson" + +msgid "relation_granddaughter" +msgstr "Granddaughter" + +msgid "relation_martial_older_brother" +msgstr "Senior Martial Brother" + +msgid "relation_martial_younger_brother" +msgstr "Junior Martial Brother" + +msgid "relation_martial_older_sister" +msgstr "Senior Martial Sister" + +msgid "relation_martial_younger_sister" +msgstr "Junior Martial Sister" diff --git a/src/i18n/locales/zh_CN/LC_MESSAGES/messages.mo b/src/i18n/locales/zh_CN/LC_MESSAGES/messages.mo index 976234c..b6c805f 100644 Binary files a/src/i18n/locales/zh_CN/LC_MESSAGES/messages.mo and b/src/i18n/locales/zh_CN/LC_MESSAGES/messages.mo differ diff --git a/src/i18n/locales/zh_CN/LC_MESSAGES/messages.po b/src/i18n/locales/zh_CN/LC_MESSAGES/messages.po index 437afd1..ac8b098 100644 --- a/src/i18n/locales/zh_CN/LC_MESSAGES/messages.po +++ b/src/i18n/locales/zh_CN/LC_MESSAGES/messages.po @@ -1924,6 +1924,45 @@ msgstr "姐姐" msgid "relation_younger_sister" msgstr "妹妹" +msgid "grand_parent" +msgstr "祖父母" + +msgid "grand_child" +msgstr "孙辈" + +msgid "martial_grandmaster" +msgstr "师祖" + +msgid "martial_grandchild" +msgstr "徒孙" + +msgid "martial_sibling" +msgstr "同门" + +msgid "relation_grandfather" +msgstr "爷爷" + +msgid "relation_grandmother" +msgstr "奶奶" + +msgid "relation_grandson" +msgstr "孙子" + +msgid "relation_granddaughter" +msgstr "孙女" + +msgid "relation_martial_older_brother" +msgstr "师兄" + +msgid "relation_martial_younger_brother" +msgstr "师弟" + +msgid "relation_martial_older_sister" +msgstr "师姐" + +msgid "relation_martial_younger_sister" +msgstr "师妹" + # Root Keys msgid "root_gold" msgstr "金灵根" diff --git a/src/i18n/locales/zh_TW/LC_MESSAGES/messages.mo b/src/i18n/locales/zh_TW/LC_MESSAGES/messages.mo index 9e7baaa..10358d6 100644 Binary files a/src/i18n/locales/zh_TW/LC_MESSAGES/messages.mo and b/src/i18n/locales/zh_TW/LC_MESSAGES/messages.mo differ diff --git a/src/i18n/locales/zh_TW/LC_MESSAGES/messages.po b/src/i18n/locales/zh_TW/LC_MESSAGES/messages.po index c174b03..e7fb22b 100644 --- a/src/i18n/locales/zh_TW/LC_MESSAGES/messages.po +++ b/src/i18n/locales/zh_TW/LC_MESSAGES/messages.po @@ -2403,3 +2403,42 @@ msgstr "冷峻簡練:短句為主,字字珠璣,如金石相擊,不做多 msgid "Fine line drawing: No decoration, capturing subtle movements and expressions to convey spirit, real and delicate." msgstr "細筆白描:不加藻飾,通過捕捉極細微的動作與神態來傳神,真實細膩。" + +msgid "grand_parent" +msgstr "祖父母" + +msgid "grand_child" +msgstr "孫輩" + +msgid "martial_grandmaster" +msgstr "師祖" + +msgid "martial_grandchild" +msgstr "徒孫" + +msgid "martial_sibling" +msgstr "同門" + +msgid "relation_grandfather" +msgstr "爺爺" + +msgid "relation_grandmother" +msgstr "奶奶" + +msgid "relation_grandson" +msgstr "孫子" + +msgid "relation_granddaughter" +msgstr "孫女" + +msgid "relation_martial_older_brother" +msgstr "師兄" + +msgid "relation_martial_younger_brother" +msgstr "師弟" + +msgid "relation_martial_older_sister" +msgstr "師姐" + +msgid "relation_martial_younger_sister" +msgstr "師妹" diff --git a/src/sim/load/load_game.py b/src/sim/load/load_game.py index c16e359..8fa6ba2 100644 --- a/src/sim/load/load_game.py +++ b/src/sim/load/load_game.py @@ -37,7 +37,7 @@ if TYPE_CHECKING: from src.classes.calendar import MonthStamp from src.classes.event import Event -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.utils.config import CONFIG diff --git a/src/sim/load_game.py b/src/sim/load_game.py index 64c8e73..489c1e0 100644 --- a/src/sim/load_game.py +++ b/src/sim/load_game.py @@ -11,7 +11,7 @@ from src.classes.calendar import MonthStamp from src.classes.avatar import Avatar from src.classes.event import Event from src.classes.sect import sects_by_id, Sect -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.sim.simulator import Simulator from src.run.load_map import load_cultivation_world_map from src.utils.config import CONFIG diff --git a/src/sim/new_avatar.py b/src/sim/new_avatar.py index 457184d..bf95898 100644 --- a/src/sim/new_avatar.py +++ b/src/sim/new_avatar.py @@ -14,7 +14,7 @@ from src.classes.age import Age from src.classes.name import get_random_name_for_sect, pick_surname_for_sect, get_random_name_with_surname from src.utils.id_generator import get_avatar_id from src.classes.sect import Sect, sects_by_id, sects_by_name -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.classes.technique import get_technique_by_sect, attribute_to_root, Technique, techniques_by_id, techniques_by_name from src.classes.weapon import Weapon, weapons_by_id, weapons_by_name from src.classes.auxiliary import Auxiliary, auxiliaries_by_id, auxiliaries_by_name diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 86815ca..d284e66 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -366,7 +366,7 @@ class Simulator: """ 关系演化阶段:检查并处理满足条件的角色关系变化 """ - from src.classes.relation_resolver import RelationResolver + from src.classes.relation.relation_resolver import RelationResolver pairs_to_resolve = [] processed_pairs = set() # (id1, id2) id1 < id2 @@ -477,9 +477,26 @@ class Simulator: # 14. 处理剩余阶段的交互计数 self._phase_handle_interactions(events, processed_event_ids) - # 15. 归档与时间推进 + # 15. (每年1月) 更新计算关系 (二阶关系) + self._phase_update_calculated_relations() + + # 16. 归档与时间推进 return self._finalize_step(events) + def _phase_update_calculated_relations(self): + """ + 每年 1 月刷新全服角色的二阶关系缓存 + """ + # 仅在 1 月执行 + if self.world.month_stamp.get_month() != Month.JANUARY: + return + + from src.classes.relation.relations import update_second_degree_relations + living_avatars = self.world.avatar_manager.get_living_avatars() + + for avatar in living_avatars: + update_second_degree_relations(avatar) + def _finalize_step(self, events: list[Event]) -> list[Event]: """ 本轮步进的最终归档:去重、入库、打日志、推进时间。 diff --git a/src/utils/protagonist.py b/src/utils/protagonist.py index 7600dad..b084bca 100644 --- a/src/utils/protagonist.py +++ b/src/utils/protagonist.py @@ -3,7 +3,7 @@ import random from src.classes.avatar import Avatar from src.classes.world import World from src.classes.calendar import MonthStamp -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.sim.new_avatar import create_avatar_from_request # ========================================== diff --git a/tests/conftest.py b/tests/conftest.py index 02c1d21..2883d03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,7 +131,7 @@ def mock_llm_managers(): with patch("src.sim.simulator.llm_ai") as mock_ai, \ patch("src.sim.simulator.process_avatar_long_term_objective", new_callable=AsyncMock) as mock_lto, \ patch("src.classes.nickname.process_avatar_nickname", new_callable=AsyncMock) as mock_nick, \ - patch("src.classes.relation_resolver.RelationResolver.run_batch", new_callable=AsyncMock) as mock_rr, \ + patch("src.classes.relation.relation_resolver.RelationResolver.run_batch", new_callable=AsyncMock) as mock_rr, \ patch("src.classes.history.HistoryManager.apply_history_influence", new_callable=AsyncMock) as mock_hist, \ patch("src.classes.story_teller.StoryTeller.tell_story", new_callable=AsyncMock) as mock_story, \ patch("src.classes.story_teller.StoryTeller.tell_gathering_story", new_callable=AsyncMock) as mock_gathering_story, \ diff --git a/tests/test_action_gift.py b/tests/test_action_gift.py index 5fb6620..a1f32e3 100644 --- a/tests/test_action_gift.py +++ b/tests/test_action_gift.py @@ -5,7 +5,7 @@ from src.classes.action_runtime import ActionResult, ActionStatus from src.classes.avatar import Avatar, Gender from src.classes.age import Age from src.classes.cultivation import Realm -from src.classes.relation import Relation +from src.classes.relation.relation import Relation from src.utils.id_generator import get_avatar_id from src.classes.calendar import create_month_stamp, Year, Month diff --git a/tests/test_death.py b/tests/test_death.py index 3b9212b..eeed7aa 100644 --- a/tests/test_death.py +++ b/tests/test_death.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock from src.classes.death_reason import DeathReason, DeathType from src.classes.death import handle_death -from src.classes.relation import Relation, get_relations_strs +from src.classes.relation.relation import Relation, get_relations_strs from src.classes.event import Event def test_death_reason_str(): diff --git a/tests/test_mutual_actions.py b/tests/test_mutual_actions.py index 8908808..b0bfab0 100644 --- a/tests/test_mutual_actions.py +++ b/tests/test_mutual_actions.py @@ -18,7 +18,7 @@ from src.classes.mutual_action.talk import Talk from src.classes.mutual_action.spar import Spar from src.classes.mutual_action.impart import Impart from src.classes.action_runtime import ActionStatus -from src.classes.relation import Relation +from src.classes.relation.relation import Relation class TestTalk: diff --git a/tests/test_new_avatar_relation.py b/tests/test_new_avatar_relation.py index 53d58c7..9773a06 100644 --- a/tests/test_new_avatar_relation.py +++ b/tests/test_new_avatar_relation.py @@ -3,7 +3,7 @@ from src.classes.world import World from src.classes.calendar import MonthStamp from src.classes.age import Age from src.classes.avatar import Avatar, Gender -from src.classes.relation import Relation, get_relation_label +from src.classes.relation.relation import Relation, get_relation_label from src.classes.cultivation import CultivationProgress, Realm from src.utils.id_generator import get_avatar_id from src.sim.new_avatar import create_random_mortal, MortalPlanner, AvatarFactory, PopulationPlanner diff --git a/tests/test_relations_i18n.py b/tests/test_relations_i18n.py new file mode 100644 index 0000000..7106b48 --- /dev/null +++ b/tests/test_relations_i18n.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import pytest +from src.classes.relation.relation import Relation +from src.classes.language import language_manager +from src.i18n import reload_translations, t + +def test_relation_i18n_zh_tw(): + # Store original language + original_lang = str(language_manager) + print(f"Original lang: {original_lang}") + + try: + # Switch to Traditional Chinese + language_manager.set_language("zh-TW") + reload_translations() + + print(f"Current lang: {str(language_manager)}") + + # Debug: try translating directly + gp_trans = t("grand_parent") + print(f"Translation of 'grand_parent': {gp_trans}") + print(f"Expected: 祖父母") + + # Test new relations + assert str(Relation.GRAND_PARENT) == "祖父母" + assert str(Relation.GRAND_CHILD) == "孫輩" + + finally: + # Restore original language + language_manager.set_language(original_lang) + reload_translations() diff --git a/tests/test_relations_logic.py b/tests/test_relations_logic.py new file mode 100644 index 0000000..94991f6 --- /dev/null +++ b/tests/test_relations_logic.py @@ -0,0 +1,91 @@ +import pytest +from src.classes.relation.relation import Relation +from src.classes.relation.relations import update_second_degree_relations, set_relation +from src.classes.avatar import Avatar, Gender +from src.classes.age import Age +from src.classes.cultivation import Realm +from src.classes.calendar import MonthStamp +from src.utils.id_generator import get_avatar_id + +def create_avatar(world, name, gender=Gender.MALE): + return Avatar( + world=world, + name=name, + id=get_avatar_id(), + birth_month_stamp=MonthStamp(0), + age=Age(20, Realm.Qi_Refinement), + gender=gender, + pos_x=0, pos_y=0 + ) + +def test_family_relations(base_world): + grandpa = create_avatar(base_world, "Grandpa") + father = create_avatar(base_world, "Father") + son = create_avatar(base_world, "Son") + daughter = create_avatar(base_world, "Daughter", Gender.FEMALE) + + # Setup relations: Grandpa -> Father -> Son/Daughter + # Father's parent is Grandpa + set_relation(father, grandpa, Relation.PARENT) + # Son's parent is Father + set_relation(son, father, Relation.PARENT) + # Daughter's parent is Father + set_relation(daughter, father, Relation.PARENT) + + # Update logic + for p in [grandpa, father, son, daughter]: + update_second_degree_relations(p) + + # Assertions + + # 1. Sibling check (Son <-> Daughter) + # Son perspective + assert son.computed_relations.get(daughter) == Relation.SIBLING + # Daughter perspective + assert daughter.computed_relations.get(son) == Relation.SIBLING + + # 2. Grandparent check (Son/Daughter -> Grandpa) + assert son.computed_relations.get(grandpa) == Relation.GRAND_PARENT + assert daughter.computed_relations.get(grandpa) == Relation.GRAND_PARENT + + # 3. Grandchild check (Grandpa -> Son/Daughter) + assert grandpa.computed_relations.get(son) == Relation.GRAND_CHILD + assert grandpa.computed_relations.get(daughter) == Relation.GRAND_CHILD + + # 4. Father should not have Sibling/Grandparent (in this limited set) + assert Relation.SIBLING not in father.computed_relations.values() + assert Relation.GRAND_PARENT not in father.computed_relations.values() + +def test_sect_relations(base_world): + master = create_avatar(base_world, "Master") + disciple_a = create_avatar(base_world, "DiscipleA") + disciple_b = create_avatar(base_world, "DiscipleB") + grand_master = create_avatar(base_world, "GrandMaster") + + # Setup: GrandMaster -> Master -> A/B + # Master is disciple of GrandMaster + set_relation(master, grand_master, Relation.MASTER) # master.relations[GM] = MASTER, GM.relations[master] = APPRENTICE + + # A is disciple of Master + set_relation(disciple_a, master, Relation.MASTER) + + # B is disciple of Master + set_relation(disciple_b, master, Relation.MASTER) + + # Update + for p in [grand_master, master, disciple_a, disciple_b]: + update_second_degree_relations(p) + + # Assertions + + # 1. Martial Sibling (A <-> B) + assert disciple_a.computed_relations.get(disciple_b) == Relation.MARTIAL_SIBLING + assert disciple_b.computed_relations.get(disciple_a) == Relation.MARTIAL_SIBLING + + # 2. Martial Grandmaster (A/B -> GrandMaster) + assert disciple_a.computed_relations.get(grand_master) == Relation.MARTIAL_GRANDMASTER + assert disciple_b.computed_relations.get(grand_master) == Relation.MARTIAL_GRANDMASTER + + # 3. Martial Grandchild (GrandMaster -> A/B) + assert grand_master.computed_relations.get(disciple_a) == Relation.MARTIAL_GRANDCHILD + assert grand_master.computed_relations.get(disciple_b) == Relation.MARTIAL_GRANDCHILD diff --git a/tests/test_save_load.py b/tests/test_save_load.py index 982b663..0b72555 100644 --- a/tests/test_save_load.py +++ b/tests/test_save_load.py @@ -140,7 +140,7 @@ def test_save_load_with_relations(temp_save_dir): world.avatar_manager.avatars[av2.id] = av2 # Add relationship - from src.classes.relation import Relation + from src.classes.relation.relation import Relation # Manually adding relation for test (usually done via helper methods) # relation value is integer diff --git a/web/src/components/game/panels/info/AvatarDetail.vue b/web/src/components/game/panels/info/AvatarDetail.vue index 2e89c2c..546f5d7 100644 --- a/web/src/components/game/panels/info/AvatarDetail.vue +++ b/web/src/components/game/panels/info/AvatarDetail.vue @@ -193,6 +193,7 @@ async function handleClearObjective() { :name="rel.name" :meta="t('game.info_panel.avatar.relation_meta', { owner: data.name, relation: rel.relation })" :sub="`${rel.sect} · ${rel.realm}`" + :type="rel.relation_type" @click="jumpToAvatar(rel.target_id)" /> diff --git a/web/src/components/game/panels/info/components/RelationRow.vue b/web/src/components/game/panels/info/components/RelationRow.vue index daaa385..1bdbc3d 100644 --- a/web/src/components/game/panels/info/components/RelationRow.vue +++ b/web/src/components/game/panels/info/components/RelationRow.vue @@ -3,6 +3,7 @@ defineProps<{ name: string; meta?: string; sub?: string; + type?: string; }>(); defineEmits(['click']); @@ -54,4 +55,3 @@ defineEmits(['click']); color: #666; } - diff --git a/web/src/types/core.ts b/web/src/types/core.ts index bcb2f8e..9c6d762 100644 --- a/web/src/types/core.ts +++ b/web/src/types/core.ts @@ -135,6 +135,7 @@ export interface RelationInfo { target_id: string; name: string; relation: string; + relation_type: string; realm: string; sect: string; }