142 lines
4.6 KiB
Python
142 lines
4.6 KiB
Python
from __future__ import annotations
|
||
|
||
import random
|
||
from typing import Optional
|
||
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.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
|
||
|
||
|
||
F_TREASURE_THEMES: list[str] = [
|
||
"误入洞府",
|
||
"巧捡奇物",
|
||
"误入试炼",
|
||
"异象出世",
|
||
"高人指点",
|
||
]
|
||
|
||
F_TECHNIQUE_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 _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 _pick_theme(kind: str) -> str:
|
||
if kind == "treasure":
|
||
return random.choice(F_TREASURE_THEMES)
|
||
return random.choice(F_TECHNIQUE_THEMES)
|
||
|
||
|
||
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:
|
||
owned_ids.add(other.treasure.id)
|
||
candidates = [t for t in treasures_by_id.values() if t.id not in owned_ids]
|
||
if not candidates:
|
||
return None
|
||
return random.choice(candidates)
|
||
|
||
|
||
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)
|
||
theme = _pick_theme(kind)
|
||
|
||
res_text: str = ""
|
||
|
||
if kind == "treasure":
|
||
tr = _get_unique_treasure_for_world(avatar)
|
||
if tr is None:
|
||
# 回退到功法
|
||
kind = "technique"
|
||
else:
|
||
avatar.treasure = tr
|
||
res_text = f"{avatar.name} 获得法宝『{tr.name}』"
|
||
|
||
if kind == "technique":
|
||
tech = get_random_upper_technique_for_avatar(avatar)
|
||
if tech is None:
|
||
# 若无可用上品,则不奖励
|
||
return []
|
||
avatar.technique = tech
|
||
res_text = f"{avatar.name} 得到上品功法『{tech.name}』"
|
||
|
||
# 生成故事(异步避免阻塞)
|
||
event_text = f"遭遇奇遇({theme}),{res_text}"
|
||
story_prompt = (
|
||
f"请据此写100~150字小故事。"
|
||
)
|
||
|
||
month_at_finish = avatar.world.month_stamp
|
||
base_event = Event(month_at_finish, event_text, related_avatars=[avatar.id])
|
||
|
||
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)
|
||
|
||
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)
|
||
|
||
schedule_background(_gen_and_push_story(), fallback=_fallback_sync)
|
||
|
||
return [base_event]
|
||
|
||
|
||
__all__ = [
|
||
"try_trigger_fortune",
|
||
]
|
||
|
||
|