update fortune

This commit is contained in:
bridge
2025-11-02 22:38:33 +08:00
parent fa13bd6cac
commit 82a9fbc024
3 changed files with 256 additions and 32 deletions

View File

@@ -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)

View File

@@ -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)

View 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