The set_relation(from, to, rel) means "from views to as rel". When avatar (student) takes master (teacher), avatar should view master as MASTER, not APPRENTICE. Before: avatar.set_relation(master, APPRENTICE) - wrong direction After: avatar.set_relation(master, MASTER) - correct direction
500 lines
16 KiB
Python
500 lines
16 KiB
Python
from __future__ import annotations
|
||
|
||
import random
|
||
from enum import Enum
|
||
from typing import Optional, Any
|
||
import asyncio
|
||
|
||
from src.utils.config import CONFIG
|
||
from src.classes.avatar import Avatar
|
||
from src.classes.event import Event
|
||
from src.classes.story_teller import StoryTeller
|
||
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.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.alignment import Alignment
|
||
from src.classes.cultivation import Realm
|
||
|
||
|
||
class FortuneKind(Enum):
|
||
"""奇遇类型"""
|
||
WEAPON = "weapon" # 兵器奇遇
|
||
AUXILIARY = "auxiliary" # 辅助装备奇遇
|
||
TECHNIQUE = "technique"
|
||
FIND_MASTER = "find_master"
|
||
SPIRIT_STONE = "spirit_stone" # 灵石奇遇
|
||
CULTIVATION = "cultivation" # 修为奇遇
|
||
|
||
|
||
F_WEAPON_THEMES: list[str] = [
|
||
"误入洞府",
|
||
"巧捡神兵",
|
||
"误入试炼",
|
||
"异象出世",
|
||
"高人赠予",
|
||
]
|
||
|
||
F_AUXILIARY_THEMES: list[str] = [
|
||
"误入洞府",
|
||
"巧捡奇物",
|
||
"误入试炼",
|
||
"异象出世",
|
||
"高人指点",
|
||
]
|
||
|
||
F_TECHNIQUE_THEMES: list[str] = [
|
||
"误入洞府",
|
||
"巧捡奇物",
|
||
"误入试炼",
|
||
"高人指点",
|
||
"玄妙感悟",
|
||
]
|
||
|
||
F_FIND_MASTER_THEMES: list[str] = [
|
||
"危难相救",
|
||
"品行打动",
|
||
"展露天赋",
|
||
"机缘巧合",
|
||
"通过考验",
|
||
]
|
||
|
||
F_SPIRIT_STONE_THEMES: list[str] = [
|
||
"偶遇灵矿",
|
||
"洞府遗财",
|
||
"击杀妖兽",
|
||
"交易获利",
|
||
"赌石得宝",
|
||
"拾遗藏宝",
|
||
]
|
||
|
||
F_CULTIVATION_THEMES: list[str] = [
|
||
"顿悟玄机",
|
||
"古碑感悟",
|
||
"服食灵药",
|
||
"秘境修炼",
|
||
"前辈灌顶",
|
||
"灵泉淬体",
|
||
"传承记忆",
|
||
]
|
||
|
||
|
||
def _has_master(avatar: Avatar) -> bool:
|
||
"""检查是否已有师傅"""
|
||
for other, rel in avatar.relations.items():
|
||
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]:
|
||
"""
|
||
在世界中寻找潜在的师傅。
|
||
规则:
|
||
1. 等级 > avatar.level + 20
|
||
2. 优先选择同宗门的高手
|
||
3. 如果没有同宗门的,选择阵营兼容的其他人
|
||
4. 不能拜敌对阵营的人为师
|
||
"""
|
||
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:
|
||
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 same_sect_candidates:
|
||
return random.choice(same_sect_candidates)
|
||
|
||
# 没有同宗门的,从其他候选中选择
|
||
if other_candidates:
|
||
return random.choice(other_candidates)
|
||
|
||
return None
|
||
|
||
|
||
def _can_get_weapon(avatar: Avatar) -> bool:
|
||
"""检查是否可以获得兵器奇遇:当前兵器是练气级(练气)时可触发"""
|
||
if avatar.weapon is None:
|
||
return True
|
||
return avatar.weapon.realm == Realm.Qi_Refinement
|
||
|
||
|
||
def _can_get_auxiliary(avatar: Avatar) -> bool:
|
||
"""检查是否可以获得辅助装备奇遇:无辅助装备或辅助装备是练气级时可触发"""
|
||
if avatar.auxiliary is None:
|
||
return True
|
||
return avatar.auxiliary.realm == Realm.Qi_Refinement
|
||
|
||
|
||
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 _can_get_spirit_stone(avatar: Avatar) -> bool:
|
||
"""检查是否可以获得灵石奇遇"""
|
||
# 任何人都可以获得灵石
|
||
return True
|
||
|
||
|
||
def _can_get_cultivation(avatar: Avatar) -> bool:
|
||
"""检查是否可以获得修为奇遇"""
|
||
# 只有未达到瓶颈的人才能获得修为
|
||
return not avatar.cultivation_progress.is_in_bottleneck()
|
||
|
||
|
||
def _choose_kind(avatar: Avatar) -> FortuneKind:
|
||
"""
|
||
从所有可能的奇遇中随机选择一个。
|
||
可能的奇遇取决于角色当前状态。
|
||
"""
|
||
possible_kinds: list[FortuneKind] = []
|
||
|
||
# 兵器奇遇:当前兵器是普通级时可触发
|
||
if _can_get_weapon(avatar):
|
||
possible_kinds.append(FortuneKind.WEAPON)
|
||
|
||
# 辅助装备奇遇:无辅助装备或辅助装备非法宝级时可触发
|
||
if _can_get_auxiliary(avatar):
|
||
possible_kinds.append(FortuneKind.AUXILIARY)
|
||
|
||
# 功法奇遇:任何人功法非上品都可以(实际获得时会有限制)
|
||
if _can_get_technique(avatar):
|
||
possible_kinds.append(FortuneKind.TECHNIQUE)
|
||
|
||
# 拜师奇遇:无师傅且世界中有合适的师傅
|
||
if _can_get_master(avatar):
|
||
possible_kinds.append(FortuneKind.FIND_MASTER)
|
||
|
||
# 灵石奇遇:任何人都可以
|
||
if _can_get_spirit_stone(avatar):
|
||
possible_kinds.append(FortuneKind.SPIRIT_STONE)
|
||
|
||
# 修为奇遇:未达到瓶颈的人可以
|
||
if _can_get_cultivation(avatar):
|
||
possible_kinds.append(FortuneKind.CULTIVATION)
|
||
|
||
if not possible_kinds:
|
||
return None
|
||
|
||
return random.choice(possible_kinds)
|
||
|
||
|
||
def _pick_theme(kind: FortuneKind) -> str:
|
||
if kind == FortuneKind.WEAPON:
|
||
return random.choice(F_WEAPON_THEMES)
|
||
elif kind == FortuneKind.AUXILIARY:
|
||
return random.choice(F_AUXILIARY_THEMES)
|
||
elif kind == FortuneKind.TECHNIQUE:
|
||
return random.choice(F_TECHNIQUE_THEMES)
|
||
elif kind == FortuneKind.FIND_MASTER:
|
||
return random.choice(F_FIND_MASTER_THEMES)
|
||
elif kind == FortuneKind.SPIRIT_STONE:
|
||
return random.choice(F_SPIRIT_STONE_THEMES)
|
||
elif kind == FortuneKind.CULTIVATION:
|
||
return random.choice(F_CULTIVATION_THEMES)
|
||
return ""
|
||
|
||
|
||
def _get_weapon_for_avatar(avatar: Avatar) -> Optional[Weapon]:
|
||
"""
|
||
获取兵器:
|
||
奇遇通常提供比当前境界更好的兵器。
|
||
如果是练气期,提供筑基期兵器。
|
||
其他境界提供同境界兵器(因为高境界兵器本身就稀有且强)。
|
||
"""
|
||
target_realm = avatar.cultivation_progress.realm
|
||
if target_realm == Realm.Qi_Refinement:
|
||
target_realm = Realm.Foundation_Establishment
|
||
|
||
return get_random_weapon_by_realm(target_realm)
|
||
|
||
|
||
def _get_auxiliary_for_avatar(avatar: Avatar) -> Optional[Auxiliary]:
|
||
"""
|
||
获取辅助装备:
|
||
规则同兵器。
|
||
"""
|
||
target_realm = avatar.cultivation_progress.realm
|
||
if target_realm == Realm.Qi_Refinement:
|
||
target_realm = Realm.Foundation_Establishment
|
||
|
||
return get_random_auxiliary_by_realm(target_realm)
|
||
|
||
|
||
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 _get_spirit_stone_amount(avatar: Avatar) -> int:
|
||
"""根据境界返回灵石数量(相当于一年狩猎售卖的收入)"""
|
||
from src.classes.cultivation import Realm
|
||
|
||
realm_ranges = {
|
||
Realm.Qi_Refinement: (20, 30),
|
||
Realm.Foundation_Establishment: (100, 150),
|
||
Realm.Core_Formation: (200, 300),
|
||
Realm.Nascent_Soul: (400, 600),
|
||
}
|
||
range_tuple = realm_ranges.get(
|
||
avatar.cultivation_progress.realm,
|
||
(20, 30) # 默认值
|
||
)
|
||
return random.randint(*range_tuple)
|
||
|
||
|
||
def get_cultivation_exp_reward(avatar: Avatar) -> int:
|
||
"""根据境界返回修为经验(相当于一年修炼的收益)"""
|
||
from src.classes.cultivation import Realm
|
||
|
||
realm_exp = {
|
||
Realm.Qi_Refinement: 600,
|
||
Realm.Foundation_Establishment: 800,
|
||
Realm.Core_Formation: 1000,
|
||
Realm.Nascent_Soul: 1200,
|
||
}
|
||
return realm_exp.get(
|
||
avatar.cultivation_progress.realm,
|
||
600 # 默认值
|
||
)
|
||
|
||
|
||
async def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||
"""
|
||
在月度结算阶段尝试触发奇遇。
|
||
规则:
|
||
- 奇遇不是一个 action;仅在条件满足时以概率触发。
|
||
- 触发条件:
|
||
* 兵器奇遇:当前兵器是普通级
|
||
* 辅助装备奇遇:无辅助装备或辅助装备非法宝级
|
||
* 功法奇遇:功法非上品(不限散修/宗门,但宗门弟子只能获得本宗门或无宗门功法)
|
||
* 拜师奇遇:无师傅且世界中有合适的师傅(优先同宗门,不能拜敌对阵营)
|
||
* 灵石奇遇:任何人都可以触发
|
||
* 修为奇遇:未达到瓶颈的人可以触发
|
||
- 结果:
|
||
* 兵器:优先法宝(世界唯一)> 宝物(可重复)
|
||
* 辅助装备:优先法宝(世界唯一)> 宝物(可重复)
|
||
* 功法:可重复,优先上品,需与灵根兼容,宗门弟子受宗门限制
|
||
* 拜师:建立师徒关系
|
||
* 灵石:根据境界获得灵石(相当于一年狩猎售卖收入)
|
||
* 修为:根据境界增加修为经验(相当于一年修炼收益)
|
||
- 故事:仅给出主旨主题,由 LLM 自由发挥生成短故事。
|
||
"""
|
||
base_prob = float(getattr(CONFIG.game, "fortune_probability", 0.0))
|
||
extra_prob = float(avatar.effects.get("extra_fortune_probability", 0.0))
|
||
prob = base_prob + extra_prob
|
||
if prob <= 0.0:
|
||
return []
|
||
|
||
if random.random() >= prob:
|
||
return []
|
||
|
||
# 从所有可能的奇遇中选择
|
||
kind = _choose_kind(avatar)
|
||
if kind is None:
|
||
return []
|
||
|
||
theme = _pick_theme(kind)
|
||
res_text: str = ""
|
||
related_avatars = [avatar.id]
|
||
actors_for_story = [avatar] # 用于生成故事的角色列表
|
||
|
||
|
||
# 导入通用决策模块
|
||
from src.classes.single_choice import handle_item_exchange
|
||
|
||
if kind == FortuneKind.WEAPON:
|
||
weapon = _get_weapon_for_avatar(avatar)
|
||
if weapon is None:
|
||
# 回退到功法
|
||
kind = FortuneKind.TECHNIQUE
|
||
theme = _pick_theme(kind)
|
||
else:
|
||
intro = f"你在奇遇中发现了{weapon.realm.value}兵器『{weapon.name}』。"
|
||
if avatar.weapon:
|
||
intro += f" 但你手中已有『{avatar.weapon.name}』。"
|
||
|
||
_, exchange_text = await handle_item_exchange(
|
||
avatar=avatar,
|
||
new_item=weapon,
|
||
item_type="weapon",
|
||
context_intro=intro,
|
||
can_sell_new=False
|
||
)
|
||
res_text = f"发现了兵器『{weapon.name}』,{exchange_text}"
|
||
|
||
if kind == FortuneKind.AUXILIARY:
|
||
auxiliary = _get_auxiliary_for_avatar(avatar)
|
||
if auxiliary is None:
|
||
# 回退到功法
|
||
kind = FortuneKind.TECHNIQUE
|
||
theme = _pick_theme(kind)
|
||
else:
|
||
intro = f"你在奇遇中发现了{auxiliary.realm.value}辅助装备『{auxiliary.name}』。"
|
||
if avatar.auxiliary:
|
||
intro += f" 但你手中已有『{avatar.auxiliary.name}』。"
|
||
|
||
_, exchange_text = await handle_item_exchange(
|
||
avatar=avatar,
|
||
new_item=auxiliary,
|
||
item_type="auxiliary",
|
||
context_intro=intro,
|
||
can_sell_new=False
|
||
)
|
||
res_text = f"发现了辅助装备『{auxiliary.name}』,{exchange_text}"
|
||
|
||
if kind == FortuneKind.TECHNIQUE:
|
||
tech = _get_fortune_technique_for_avatar(avatar)
|
||
if tech is None:
|
||
return []
|
||
|
||
intro = f"你在奇遇中领悟了上品功法『{tech.name}』。"
|
||
if avatar.technique:
|
||
intro += f" 这与你当前主修的『{avatar.technique.name}』冲突。"
|
||
|
||
_, exchange_text = await handle_item_exchange(
|
||
avatar=avatar,
|
||
new_item=tech,
|
||
item_type="technique",
|
||
context_intro=intro,
|
||
can_sell_new=False
|
||
)
|
||
res_text = f"领悟了功法『{tech.name}』,{exchange_text}"
|
||
|
||
elif kind == FortuneKind.FIND_MASTER:
|
||
master = _find_potential_master(avatar)
|
||
if master is None:
|
||
# 找不到合适的师傅
|
||
return []
|
||
# 建立师徒关系:avatar 是徒弟,master 是师傅
|
||
# avatar 视 master 为 MASTER,master 视 avatar 为 APPRENTICE(自动设置对偶)。
|
||
avatar.set_relation(master, Relation.MASTER)
|
||
res_text = f"{avatar.name} 拜 {master.name} 为师"
|
||
related_avatars.append(master.id)
|
||
actors_for_story = [avatar, master] # 拜师奇遇需要两个人的信息
|
||
|
||
elif kind == FortuneKind.SPIRIT_STONE:
|
||
amount = _get_spirit_stone_amount(avatar)
|
||
avatar.magic_stone.value += amount
|
||
res_text = f"{avatar.name} 获得灵石 {amount} 枚"
|
||
|
||
elif kind == FortuneKind.CULTIVATION:
|
||
exp_gain = get_cultivation_exp_reward(avatar)
|
||
avatar.cultivation_progress.add_exp(exp_gain)
|
||
res_text = f"{avatar.name} 修为增长 {exp_gain} 点"
|
||
|
||
# 生成故事(异步,等待完成)
|
||
event_text = f"遭遇奇遇({theme}),{res_text}"
|
||
story_prompt = "请据此写100~300字小故事。"
|
||
|
||
month_at_finish = avatar.world.month_stamp
|
||
base_event = Event(month_at_finish, event_text, related_avatars=related_avatars, is_major=True)
|
||
|
||
# 生成故事事件
|
||
# 奇遇强制单人模式,不改变关系(因为关系已经在硬逻辑中处理了)
|
||
story = await StoryTeller.tell_story(event_text, res_text, *actors_for_story, prompt=story_prompt, allow_relation_changes=False)
|
||
story_event = Event(month_at_finish, story, related_avatars=related_avatars, is_story=True)
|
||
|
||
# 返回基础事件和故事事件
|
||
return [base_event, story_event]
|
||
|
||
|
||
__all__ = [
|
||
"try_trigger_fortune",
|
||
"get_cultivation_exp_reward",
|
||
]
|