From 7630174820d2dadd63b931a493e2cffedf947d8a Mon Sep 17 00:00:00 2001 From: 4thfever Date: Thu, 5 Feb 2026 22:14:44 +0800 Subject: [PATCH] Feat/relation (#139) * update relation * feat: add relation_type to avatar info structure and update related components - Added `relation_type` to the avatar structured info in `info_presenter.py`. - Updated `AvatarDetail.vue` to utilize the new `relation_type` for displaying avatar relationships. - Modified `RelationRow.vue` to accept `type` as a prop for enhanced relationship representation. - Updated `core.ts` to include `relation_type` in the `RelationInfo` interface. Closes # --- src/classes/avatar/core.py | 10 +- src/classes/avatar/info_presenter.py | 5 +- src/classes/fortune.py | 2 +- src/classes/mutual_action/conversation.py | 2 +- src/classes/mutual_action/impart.py | 2 +- src/classes/mutual_action/mutual_action.py | 4 +- src/classes/{ => relation}/relation.py | 97 +++++++++++++----- .../{ => relation}/relation_resolver.py | 11 +- src/classes/{ => relation}/relations.py | 81 ++++++++++++++- src/classes/story_teller.py | 2 +- src/classes/tribulation.py | 2 +- .../locales/en_US/LC_MESSAGES/messages.mo | Bin 62014 -> 62726 bytes .../locales/en_US/LC_MESSAGES/messages.po | 39 +++++++ .../locales/zh_CN/LC_MESSAGES/messages.mo | Bin 57863 -> 58454 bytes .../locales/zh_CN/LC_MESSAGES/messages.po | 39 +++++++ .../locales/zh_TW/LC_MESSAGES/messages.mo | Bin 59670 -> 60261 bytes .../locales/zh_TW/LC_MESSAGES/messages.po | 39 +++++++ src/sim/load/load_game.py | 2 +- src/sim/load_game.py | 2 +- src/sim/new_avatar.py | 2 +- src/sim/simulator.py | 21 +++- src/utils/protagonist.py | 2 +- tests/conftest.py | 2 +- tests/test_action_gift.py | 2 +- tests/test_death.py | 2 +- tests/test_mutual_actions.py | 2 +- tests/test_new_avatar_relation.py | 2 +- tests/test_relations_i18n.py | 31 ++++++ tests/test_relations_logic.py | 91 ++++++++++++++++ tests/test_save_load.py | 2 +- .../game/panels/info/AvatarDetail.vue | 1 + .../panels/info/components/RelationRow.vue | 2 +- web/src/types/core.ts | 1 + 33 files changed, 437 insertions(+), 65 deletions(-) rename src/classes/{ => relation}/relation.py (68%) rename src/classes/{ => relation}/relation_resolver.py (95%) rename src/classes/{ => relation}/relations.py (70%) create mode 100644 tests/test_relations_i18n.py create mode 100644 tests/test_relations_logic.py 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 0df47fd2d99240c4a44f5bf78bc8a39ccdc6f257..1519ac0dfba6617208134b13ebe95770c6871e15 100644 GIT binary patch delta 12654 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 delta 11976 zcmYM)d3cY<`p5A{LPQ8j5Q)fwL?V)i*ka9s*!P1dDIrL-jy6SG#TQkzZ>0#SqNN12 zRkgM%TKlf{rBxL5L)F^Fd4DoZ4ZLvb+r;}oodOHuFb#VGt4Bhjm-xb#Fw!(-a}q->eq6dvN*{49tIK5#1Je%P52;c zA@>Snm*aRRmS_%pr}PoXciM9uUI)C_+_rS=v^V9~m!J{p6G+aQmeZ1l!_Y>Ne05}%?L z;$F`zEC`1aH^aX84Z7-6sL1~{!7iwo&O!~a920RjYGp4l30qOAEtrIAzW@WU0K;)F zDwDUd0{S*^oN8Ddn_(wZhUPY4|JBhtD$3zb^u$xx2rnUva01A07aZ-sG(2EqAA;({^-;GX6GL$fhT|uw0r#L%d=`h|BOHwVTbfKC#R0^Tt;nJ_X*vb1 z;3L!omZ7fIdi1~>s0{pWpFhED;-ak`#|MXCF&u+h$OQDo$ygf~q53_B+M>(IoSY!i z?x91{kAeZc$JMqS~1i*GC<`rl^&qp$=g>s@)K*f}>CwT7p`@3haSfP=~H`d$XV@ zRHjnU3%j5@X0#{&9Vui|!8SV^ume6tvh1W0aMPTb$U%0FV&Mmcm$Z`V$lRUASPCbh zCOi){ft9E;vKuSo�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 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 976234cf47ff37d5125b398ca71d4a78a766bebb..b6c805f5b6ac085268e4764c9b323defb7cb821a 100644 GIT binary patch delta 12528 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 delta 11969 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 Av;Y7A 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 9e7baaa7c6b66c0016ae6470668709db20b3043d..10358d6095556cccc537f5f7fa2a834cba2c929e 100644 GIT binary patch delta 12611 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 delta 12073 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 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; }