diff --git a/src/classes/fortune.py b/src/classes/fortune.py index 563cd2f..56ef462 100644 --- a/src/classes/fortune.py +++ b/src/classes/fortune.py @@ -11,9 +11,17 @@ from src.classes.event import Event from src.classes.story_teller import StoryTeller from src.classes.action.event_helper import EventHelper from src.utils.asyncio_utils import schedule_background -from src.classes.technique import TechniqueGrade, get_random_upper_technique_for_avatar +from src.classes.technique import ( + TechniqueGrade, + get_random_upper_technique_for_avatar, + techniques_by_id, + Technique, + is_attribute_compatible_with_root, + TechniqueAttribute, +) from src.classes.treasure import Treasure, treasures_by_id from src.classes.relation import Relation +from src.classes.alignment import Alignment class FortuneKind(Enum): @@ -48,39 +56,89 @@ F_FIND_MASTER_THEMES: list[str] = [ ] -def _is_rogue_and_under_equipped(avatar: Avatar) -> bool: - # 必须散修;法宝为空 或 功法非上品 - if avatar.sect is not None: - return False - has_no_treasure = avatar.treasure is None - is_tech_lower = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER) - return has_no_treasure or is_tech_lower - - def _has_master(avatar: Avatar) -> bool: """检查是否已有师傅""" for other, rel in avatar.relations.items(): - if rel == Relation.MASTER: + if rel == Relation.APPRENTICE: return True return False +def _is_alignment_compatible(avatar: Avatar, other: Avatar) -> bool: + """检查两个角色的阵营是否兼容(不是敌对关系)""" + from src.classes.alignment import Alignment + if avatar.alignment is None or other.alignment is None: + return True + # 正邪不相容 + if avatar.alignment == Alignment.RIGHTEOUS and other.alignment == Alignment.EVIL: + return False + if avatar.alignment == Alignment.EVIL and other.alignment == Alignment.RIGHTEOUS: + return False + return True + + def _find_potential_master(avatar: Avatar) -> Optional[Avatar]: """ 在世界中寻找潜在的师傅。 - 条件:等级 > avatar.level + 20 + 规则: + 1. 等级 > avatar.level + 20 + 2. 优先选择同宗门的高手 + 3. 如果没有同宗门的,选择阵营兼容的其他人 + 4. 不能拜敌对阵营的人为师 """ - candidates: list[Avatar] = [] + same_sect_candidates: list[Avatar] = [] + other_candidates: list[Avatar] = [] + for other in avatar.world.avatar_manager.avatars.values(): if other is avatar: continue + + # 等级差检查 level_diff = other.cultivation_progress.level - avatar.cultivation_progress.level - if level_diff >= 20: - candidates.append(other) + if level_diff < 20: + continue + + # 阵营兼容性检查 + if not _is_alignment_compatible(avatar, other): + continue + + # 同宗门优先 + if avatar.sect is not None and other.sect == avatar.sect: + same_sect_candidates.append(other) + else: + other_candidates.append(other) - if not candidates: - return None - return random.choice(candidates) + # 优先从同宗门选择 + if same_sect_candidates: + return random.choice(same_sect_candidates) + + # 没有同宗门的,从其他候选中选择 + if other_candidates: + return random.choice(other_candidates) + + return None + + +def _can_get_treasure(avatar: Avatar) -> bool: + """检查是否可以获得法宝奇遇""" + return avatar.treasure is None + + +def _can_get_technique(avatar: Avatar) -> bool: + """ + 检查是否可以获得功法奇遇 + - 任何人功法非上品都可以触发 + - 但实际能否获得功法,在获取时会有额外检查(宗门弟子有限制) + """ + tech_not_upper = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER) + return tech_not_upper + + +def _can_get_master(avatar: Avatar) -> bool: + """检查是否可以获得拜师奇遇""" + if _has_master(avatar): + return False + return _find_potential_master(avatar) is not None def _choose_kind(avatar: Avatar) -> FortuneKind: @@ -90,20 +148,17 @@ def _choose_kind(avatar: Avatar) -> FortuneKind: """ possible_kinds: list[FortuneKind] = [] - # 法宝奇遇:散修且无法宝 - if avatar.sect is None and avatar.treasure is None: + # 法宝奇遇:任何人无法宝都可以 + if _can_get_treasure(avatar): possible_kinds.append(FortuneKind.TREASURE) - # 功法奇遇:散修且功法非上品 - if avatar.sect is None: - tech_not_upper = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER) - if tech_not_upper: - possible_kinds.append(FortuneKind.TECHNIQUE) + # 功法奇遇:任何人功法非上品都可以(实际获得时会有限制) + if _can_get_technique(avatar): + possible_kinds.append(FortuneKind.TECHNIQUE) # 拜师奇遇:无师傅且世界中有合适的师傅 - if not _has_master(avatar): - if _find_potential_master(avatar) is not None: - possible_kinds.append(FortuneKind.FIND_MASTER) + if _can_get_master(avatar): + possible_kinds.append(FortuneKind.FIND_MASTER) if not possible_kinds: return None @@ -122,7 +177,7 @@ def _pick_theme(kind: FortuneKind) -> str: def _get_unique_treasure_for_world(avatar: Avatar) -> Optional[Treasure]: - # 世界唯一法宝:从全量里挑选一个未被任何人持有的 + """获取世界唯一法宝:从全量里挑选一个未被任何人持有的""" owned_ids: set[int] = set() for other in avatar.world.avatar_manager.avatars.values(): if other.treasure is not None: @@ -133,13 +188,68 @@ def _get_unique_treasure_for_world(avatar: Avatar) -> Optional[Treasure]: return random.choice(candidates) +def _get_fortune_technique_for_avatar(avatar: Avatar) -> Optional[Technique]: + """ + 为奇遇获取功法。 + 规则: + 1. 散修:可以获得任何上品功法(与灵根/阵营/condition兼容) + 2. 宗门弟子:只能获得本宗门或无宗门的上品功法 + """ + candidates: list[Technique] = [] + + # 确定允许的宗门范围 + allowed_sects: set[Optional[str]] = {None, ""} + if avatar.sect is not None: + sect_name = avatar.sect.name.strip() if avatar.sect.name else None + if sect_name: + allowed_sects.add(sect_name) + + # 筛选功法 + for t in techniques_by_id.values(): + # 必须是上品 + if t.grade != TechniqueGrade.UPPER: + continue + + # 宗门限制:宗门弟子只能获得本宗门或无宗门的功法 + tech_sect = t.sect.strip() if t.sect else None + if tech_sect not in allowed_sects: + continue + + # condition 检查 + if not t.is_allowed_for(avatar): + continue + + # 邪功法只能邪道修士修炼 + if t.attribute == TechniqueAttribute.EVIL and avatar.alignment != Alignment.EVIL: + continue + + # 灵根兼容性 + if not is_attribute_compatible_with_root(t.attribute, avatar.root): + continue + + candidates.append(t) + + if not candidates: + return None + + # 按权重随机选择 + weights = [max(0.0, t.weight) for t in candidates] + return random.choices(candidates, weights=weights, k=1)[0] + + def try_trigger_fortune(avatar: Avatar) -> list[Event]: """ 在月度结算阶段尝试触发奇遇。 规则: - 奇遇不是一个 action;仅在条件满足时以概率触发。 - - 触发条件:散修且(无法宝 或 功法非上品),或者无师傅。 - - 结果:先决定奖励类型(法宝/功法/拜师),法宝世界唯一且不可重复;功法可重复但优先上品且需与灵根兼容;拜师建立师徒关系。 + - 触发条件: + * 法宝奇遇:无法宝(不限散修/宗门) + * 功法奇遇:功法非上品(不限散修/宗门,但宗门弟子只能获得本宗门或无宗门功法) + * 拜师奇遇:无师傅且世界中有合适的师傅(优先同宗门,不能拜敌对阵营) + - 结果: + * 法宝:世界唯一且不可重复 + * 功法:可重复,优先上品,需与灵根兼容,宗门弟子受宗门限制 + * 拜师:建立师徒关系 - 故事:仅给出主旨主题,由 LLM 自由发挥生成短故事。 """ prob = float(getattr(CONFIG.game, "fortune_probability", 0.0)) @@ -170,9 +280,9 @@ def try_trigger_fortune(avatar: Avatar) -> list[Event]: res_text = f"{avatar.name} 获得法宝『{tr.name}』" if kind == FortuneKind.TECHNIQUE: - tech = get_random_upper_technique_for_avatar(avatar) + tech = _get_fortune_technique_for_avatar(avatar) if tech is None: - # 若无可用上品,则不奖励 + # 若无可用上品功法(宗门弟子可能因宗门限制而找不到),则不奖励 return [] avatar.technique = tech res_text = f"{avatar.name} 得到上品功法『{tech.name}』" @@ -182,8 +292,8 @@ def try_trigger_fortune(avatar: Avatar) -> list[Event]: if master is None: # 找不到合适的师傅 return [] - # 建立师徒关系 - avatar.set_relation(master, Relation.MASTER) + # 建立师徒关系:avatar 是徒弟,master 是师傅 + avatar.set_relation(master, Relation.APPRENTICE) res_text = f"{avatar.name} 拜 {master.name} 为师" related_avatars.append(master.id) actors_for_story = [avatar, master] # 拜师奇遇需要两个人的信息 diff --git a/src/classes/mutual_action/impart.py b/src/classes/mutual_action/impart.py index 0ae4422..7d75afd 100644 --- a/src/classes/mutual_action/impart.py +++ b/src/classes/mutual_action/impart.py @@ -38,9 +38,9 @@ class Impart(MutualAction): def _can_start(self, target: "Avatar") -> tuple[bool, str]: """检查传道特有的启动条件""" - # 检查是否是师徒关系 + # 检查是否是师徒关系:师傅对徒弟的关系应该是 MASTER relation = self.avatar.get_relation(target) - if relation != Relation.APPRENTICE: + if relation != Relation.MASTER: return False, "目标不是自己的徒弟" # 检查等级差