Files
cultivation-world-simulator/src/classes/fortune.py
2025-10-23 00:50:52 +08:00

142 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
]