update fortune
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
|
||||
@@ -12,6 +13,14 @@ 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.treasure import Treasure, treasures_by_id
|
||||
from src.classes.relation import Relation
|
||||
|
||||
|
||||
class FortuneKind(Enum):
|
||||
"""奇遇类型"""
|
||||
TREASURE = "treasure"
|
||||
TECHNIQUE = "technique"
|
||||
FIND_MASTER = "find_master"
|
||||
|
||||
|
||||
F_TREASURE_THEMES: list[str] = [
|
||||
@@ -30,6 +39,14 @@ F_TECHNIQUE_THEMES: list[str] = [
|
||||
"玄妙感悟",
|
||||
]
|
||||
|
||||
F_FIND_MASTER_THEMES: list[str] = [
|
||||
"危难相救",
|
||||
"品行打动",
|
||||
"展露天赋",
|
||||
"机缘巧合",
|
||||
"通过考验",
|
||||
]
|
||||
|
||||
|
||||
def _is_rogue_and_under_equipped(avatar: Avatar) -> bool:
|
||||
# 必须散修;法宝为空 或 功法非上品
|
||||
@@ -40,23 +57,68 @@ def _is_rogue_and_under_equipped(avatar: Avatar) -> bool:
|
||||
return has_no_treasure or is_tech_lower
|
||||
|
||||
|
||||
def _choose_kind(avatar: Avatar) -> str:
|
||||
# 如果无法宝,偏向法宝;否则若功法非上品,偏向功法;否则随机
|
||||
no_treasure = avatar.treasure is None
|
||||
tech_not_upper = (avatar.technique is None) or (avatar.technique.grade is not TechniqueGrade.UPPER)
|
||||
if no_treasure and tech_not_upper:
|
||||
return random.choice(["treasure", "technique"]) # 两者都缺,随机其一
|
||||
if no_treasure:
|
||||
return "treasure"
|
||||
if tech_not_upper:
|
||||
return "technique"
|
||||
return random.choice(["treasure", "technique"])
|
||||
def _has_master(avatar: Avatar) -> bool:
|
||||
"""检查是否已有师傅"""
|
||||
for other, rel in avatar.relations.items():
|
||||
if rel == Relation.MASTER:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _pick_theme(kind: str) -> str:
|
||||
if kind == "treasure":
|
||||
def _find_potential_master(avatar: Avatar) -> Optional[Avatar]:
|
||||
"""
|
||||
在世界中寻找潜在的师傅。
|
||||
条件:等级 > avatar.level + 20
|
||||
"""
|
||||
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 not candidates:
|
||||
return None
|
||||
return random.choice(candidates)
|
||||
|
||||
|
||||
def _choose_kind(avatar: Avatar) -> FortuneKind:
|
||||
"""
|
||||
从所有可能的奇遇中随机选择一个。
|
||||
可能的奇遇取决于角色当前状态。
|
||||
"""
|
||||
possible_kinds: list[FortuneKind] = []
|
||||
|
||||
# 法宝奇遇:散修且无法宝
|
||||
if avatar.sect is None and avatar.treasure is None:
|
||||
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 not _has_master(avatar):
|
||||
if _find_potential_master(avatar) is not None:
|
||||
possible_kinds.append(FortuneKind.FIND_MASTER)
|
||||
|
||||
if not possible_kinds:
|
||||
return None
|
||||
|
||||
return random.choice(possible_kinds)
|
||||
|
||||
|
||||
def _pick_theme(kind: FortuneKind) -> str:
|
||||
if kind == FortuneKind.TREASURE:
|
||||
return random.choice(F_TREASURE_THEMES)
|
||||
return random.choice(F_TECHNIQUE_THEMES)
|
||||
elif kind == FortuneKind.TECHNIQUE:
|
||||
return random.choice(F_TECHNIQUE_THEMES)
|
||||
elif kind == FortuneKind.FIND_MASTER:
|
||||
return random.choice(F_FIND_MASTER_THEMES)
|
||||
return ""
|
||||
|
||||
|
||||
def _get_unique_treasure_for_world(avatar: Avatar) -> Optional[Treasure]:
|
||||
@@ -76,33 +138,38 @@ def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
在月度结算阶段尝试触发奇遇。
|
||||
规则:
|
||||
- 奇遇不是一个 action;仅在条件满足时以概率触发。
|
||||
- 触发条件:散修,且(无法宝 或 功法非上品)。
|
||||
- 结果:先决定奖励类型(法宝/功法),法宝世界唯一且不可重复;功法可重复但优先上品且需与灵根兼容。
|
||||
- 触发条件:散修且(无法宝 或 功法非上品),或者无师傅。
|
||||
- 结果:先决定奖励类型(法宝/功法/拜师),法宝世界唯一且不可重复;功法可重复但优先上品且需与灵根兼容;拜师建立师徒关系。
|
||||
- 故事:仅给出主旨主题,由 LLM 自由发挥生成短故事。
|
||||
"""
|
||||
prob = float(getattr(CONFIG.game, "fortune_probability", 0.0))
|
||||
if prob <= 0.0:
|
||||
return []
|
||||
if not _is_rogue_and_under_equipped(avatar):
|
||||
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] # 用于生成故事的角色列表
|
||||
|
||||
if kind == "treasure":
|
||||
if kind == FortuneKind.TREASURE:
|
||||
tr = _get_unique_treasure_for_world(avatar)
|
||||
if tr is None:
|
||||
# 回退到功法
|
||||
kind = "technique"
|
||||
kind = FortuneKind.TECHNIQUE
|
||||
theme = _pick_theme(kind)
|
||||
else:
|
||||
avatar.treasure = tr
|
||||
res_text = f"{avatar.name} 获得法宝『{tr.name}』"
|
||||
|
||||
if kind == "technique":
|
||||
if kind == FortuneKind.TECHNIQUE:
|
||||
tech = get_random_upper_technique_for_avatar(avatar)
|
||||
if tech is None:
|
||||
# 若无可用上品,则不奖励
|
||||
@@ -110,24 +177,42 @@ def try_trigger_fortune(avatar: Avatar) -> list[Event]:
|
||||
avatar.technique = tech
|
||||
res_text = f"{avatar.name} 得到上品功法『{tech.name}』"
|
||||
|
||||
elif kind == FortuneKind.FIND_MASTER:
|
||||
master = _find_potential_master(avatar)
|
||||
if master is None:
|
||||
# 找不到合适的师傅
|
||||
return []
|
||||
# 建立师徒关系
|
||||
avatar.set_relation(master, Relation.MASTER)
|
||||
res_text = f"{avatar.name} 拜 {master.name} 为师"
|
||||
related_avatars.append(master.id)
|
||||
actors_for_story = [avatar, master] # 拜师奇遇需要两个人的信息
|
||||
|
||||
# 生成故事(异步避免阻塞)
|
||||
event_text = f"遭遇奇遇({theme}),{res_text}"
|
||||
story_prompt = (
|
||||
f"请据此写100~150字小故事。"
|
||||
)
|
||||
story_prompt = "请据此写100~150字小故事。"
|
||||
|
||||
month_at_finish = avatar.world.month_stamp
|
||||
base_event = Event(month_at_finish, event_text, related_avatars=[avatar.id])
|
||||
base_event = Event(month_at_finish, event_text, related_avatars=related_avatars)
|
||||
|
||||
async def _gen_and_push_story():
|
||||
story = await StoryTeller.tell_from_actors_async(event_text, res_text, avatar, prompt=story_prompt)
|
||||
story_event = Event(month_at_finish, story, related_avatars=[avatar.id])
|
||||
EventHelper.push_self(story_event, avatar, to_sidebar=True)
|
||||
# 拜师奇遇传入两个角色,其他奇遇传入一个角色
|
||||
story = await StoryTeller.tell_from_actors_async(event_text, res_text, *actors_for_story, prompt=story_prompt)
|
||||
story_event = Event(month_at_finish, story, related_avatars=related_avatars)
|
||||
# 根据涉及角色数量推送事件
|
||||
if len(actors_for_story) == 1:
|
||||
EventHelper.push_self(story_event, avatar, to_sidebar=True)
|
||||
else:
|
||||
# 拜师奇遇涉及两个角色
|
||||
EventHelper.push_pair(story_event, initiator=avatar, target=actors_for_story[1], to_sidebar_once=True)
|
||||
|
||||
def _fallback_sync():
|
||||
story = StoryTeller.tell_from_actors(event_text, res_text, avatar, prompt=story_prompt)
|
||||
story_event = Event(month_at_finish, story, related_avatars=[avatar.id])
|
||||
EventHelper.push_self(story_event, avatar, to_sidebar=True)
|
||||
story = StoryTeller.tell_from_actors(event_text, res_text, *actors_for_story, prompt=story_prompt)
|
||||
story_event = Event(month_at_finish, story, related_avatars=related_avatars)
|
||||
if len(actors_for_story) == 1:
|
||||
EventHelper.push_self(story_event, avatar, to_sidebar=True)
|
||||
else:
|
||||
EventHelper.push_pair(story_event, initiator=avatar, target=actors_for_story[1], to_sidebar_once=True)
|
||||
|
||||
schedule_background(_gen_and_push_story(), fallback=_fallback_sync)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from .attack import Attack
|
||||
from .conversation import Conversation
|
||||
from .dual_cultivation import DualCultivation
|
||||
from .talk import Talk
|
||||
from .impart import Impart
|
||||
from src.classes.action.registry import register_action
|
||||
|
||||
__all__ = [
|
||||
@@ -15,6 +16,7 @@ __all__ = [
|
||||
"Conversation",
|
||||
"DualCultivation",
|
||||
"Talk",
|
||||
"Impart",
|
||||
]
|
||||
|
||||
# 注册 mutual actions(均为实际动作)
|
||||
@@ -23,5 +25,6 @@ register_action(actual=True)(Attack)
|
||||
register_action(actual=True)(Conversation)
|
||||
register_action(actual=True)(DualCultivation)
|
||||
register_action(actual=True)(Talk)
|
||||
register_action(actual=True)(Impart)
|
||||
|
||||
|
||||
|
||||
136
src/classes/mutual_action/impart.py
Normal file
136
src/classes/mutual_action/impart.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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.utils.config import CONFIG
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
|
||||
|
||||
@cooldown_action
|
||||
class Impart(MutualAction):
|
||||
"""传道:师傅对徒弟传授修炼经验。
|
||||
|
||||
- 仅限发起方是目标的师傅(检查师徒关系)
|
||||
- 师傅等级必须大于徒弟等级20级以上
|
||||
- 目标在交互范围内
|
||||
- 目标可以选择 接受 或 拒绝
|
||||
- 若接受:徒弟获得大量修为(相当于在灵气密度5的地方修炼的4倍,即2000经验)
|
||||
"""
|
||||
|
||||
ACTION_NAME = "传道"
|
||||
COMMENT = "师傅向徒弟传授修炼经验,徒弟可获得大量修为"
|
||||
DOABLES_REQUIREMENTS = "发起者是目标的师傅;师傅等级 > 徒弟等级 + 20;目标在交互范围内;不能连续执行"
|
||||
PARAMS = {"target_avatar": "AvatarName"}
|
||||
FEEDBACK_ACTIONS = ["Accept", "Reject"]
|
||||
STORY_PROMPT: str | None = "师傅向徒弟传道授业,描绘一段温馨的师徒传承场景,体现师傅的循循善诱与徒弟的虚心求教。100~150字。"
|
||||
# 传道冷却:6个月
|
||||
ACTION_CD_MONTHS: int = 6
|
||||
|
||||
def _get_template_path(self) -> Path:
|
||||
return CONFIG.paths.templates / "mutual_action.txt"
|
||||
|
||||
def _can_start(self, target: "Avatar") -> tuple[bool, str]:
|
||||
"""检查传道特有的启动条件"""
|
||||
# 检查是否是师徒关系
|
||||
relation = self.avatar.get_relation(target)
|
||||
if relation != Relation.APPRENTICE:
|
||||
return False, "目标不是自己的徒弟"
|
||||
|
||||
# 检查等级差
|
||||
level_diff = self.avatar.cultivation_progress.level - target.cultivation_progress.level
|
||||
if level_diff < 20:
|
||||
return False, f"等级差不足20级(当前差距:{level_diff}级)"
|
||||
|
||||
return True, ""
|
||||
|
||||
def start(self, target_avatar: "Avatar|str") -> Event:
|
||||
target = self._get_target_avatar(target_avatar)
|
||||
target_name = target.name if target is not None else str(target_avatar)
|
||||
rel_ids = [self.avatar.id]
|
||||
if target is not None:
|
||||
rel_ids.append(target.id)
|
||||
event = Event(
|
||||
self.world.month_stamp,
|
||||
f"{self.avatar.name} 向徒弟 {target_name} 传道授业",
|
||||
related_avatars=rel_ids
|
||||
)
|
||||
# 仅写入历史
|
||||
self.avatar.add_event(event, to_sidebar=False)
|
||||
if target is not None:
|
||||
target.add_event(event, to_sidebar=False)
|
||||
# 记录开始文本用于故事生成
|
||||
self._start_event_content = event.content
|
||||
# 初始化内部标记
|
||||
self._impart_success = False
|
||||
self._impart_exp_gain = 0
|
||||
return event
|
||||
|
||||
def _settle_feedback(self, target_avatar: "Avatar", feedback_name: str) -> None:
|
||||
fb = str(feedback_name).strip()
|
||||
if fb == "Accept":
|
||||
# 接受则当场结算修为收益(徒弟获得)
|
||||
self._apply_impart_gain(target_avatar)
|
||||
self._impart_success = True
|
||||
else:
|
||||
# 拒绝
|
||||
self._impart_success = False
|
||||
|
||||
def _apply_impart_gain(self, target: "Avatar") -> None:
|
||||
# 传道经验:相当于在灵气密度5的地方修炼的4倍
|
||||
# base_exp = 100, density = 5, 倍数 = 4
|
||||
# 总经验 = 100 * 5 * 4 = 2000
|
||||
exp_gain = 100 * 5 * 4
|
||||
target.cultivation_progress.add_exp(exp_gain)
|
||||
self._impart_exp_gain = exp_gain
|
||||
|
||||
def finish(self, target_avatar: "Avatar|str") -> list[Event]:
|
||||
target = self._get_target_avatar(target_avatar)
|
||||
events: list[Event] = []
|
||||
success = self._impart_success
|
||||
if target is None:
|
||||
return events
|
||||
|
||||
if success:
|
||||
gain = int(self._impart_exp_gain)
|
||||
result_text = f"{self.avatar.name} 向 {target.name} 传道,{target.name} 获得修为经验 +{gain} 点"
|
||||
result_event = Event(
|
||||
self.world.month_stamp,
|
||||
result_text,
|
||||
related_avatars=[self.avatar.id, target.id]
|
||||
)
|
||||
events.append(result_event)
|
||||
|
||||
# 生成师徒传道小故事
|
||||
from src.classes.story_teller import StoryTeller
|
||||
start_text = self._start_event_content or result_event.content
|
||||
story = StoryTeller.tell_from_actors(
|
||||
start_text,
|
||||
result_text,
|
||||
self.avatar,
|
||||
target,
|
||||
prompt=self.STORY_PROMPT
|
||||
)
|
||||
story_event = Event(
|
||||
self.world.month_stamp,
|
||||
story,
|
||||
related_avatars=[self.avatar.id, target.id]
|
||||
)
|
||||
events.append(story_event)
|
||||
else:
|
||||
result_text = f"{target.name} 婉拒了 {self.avatar.name} 的传道"
|
||||
result_event = Event(
|
||||
self.world.month_stamp,
|
||||
result_text,
|
||||
related_avatars=[self.avatar.id, target.id]
|
||||
)
|
||||
events.append(result_event)
|
||||
|
||||
return events
|
||||
|
||||
Reference in New Issue
Block a user