diff --git a/src/classes/effects.py b/src/classes/effects.py index 4b6d7d1..95ea95c 100644 --- a/src/classes/effects.py +++ b/src/classes/effects.py @@ -42,18 +42,6 @@ EXTRA_MAX_HP = "extra_max_hp" - 大量: 200+ """ -EXTRA_MAX_MP = "extra_max_mp" -""" -额外最大灵力值 -类型: int -结算: src/classes/avatar.py (__post_init__) -说明: 增加角色的最大灵力值上限。 -数值参考: - - 微量: 10~30 - - 中量: 50~100 (练气期基础MP约100) - - 大量: 200+ -""" - EXTRA_OBSERVATION_RADIUS = "extra_observation_radius" """ 额外观察半径 @@ -372,7 +360,6 @@ ALL_EFFECTS = [ # 战斗相关 "extra_battle_strength_points", # int - 额外战斗力 "extra_max_hp", # int - 额外最大生命值 - "extra_max_mp", # int - 额外最大灵力值 "extra_observation_radius", # int - 额外观察半径 "damage_reduction", # float - 伤害减免 "realm_suppression_bonus", # float - 境界压制加成 @@ -403,6 +390,7 @@ ALL_EFFECTS = [ # 奇遇相关 "extra_fortune_probability", # float - 额外奇遇概率 + "extra_misfortune_probability", # float - 额外霉运概率 # 兵器相关 "extra_weapon_proficiency_gain", # float - 额外兵器熟练度增长倍率 diff --git a/src/classes/fortune.py b/src/classes/fortune.py index 32b3887..483930c 100644 --- a/src/classes/fortune.py +++ b/src/classes/fortune.py @@ -365,7 +365,7 @@ def _get_spirit_stone_amount(avatar: Avatar) -> int: return random.randint(*range_tuple) -def _get_cultivation_exp(avatar: Avatar) -> int: +def get_cultivation_exp_reward(avatar: Avatar) -> int: """根据境界返回修为经验(相当于一年修炼的收益)""" from src.classes.cultivation import Realm @@ -523,13 +523,13 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]: res_text = f"{avatar.name} 获得灵石 {amount} 枚" elif kind == FortuneKind.CULTIVATION: - exp_gain = _get_cultivation_exp(avatar) + 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~150字小故事。" + 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) @@ -545,4 +545,5 @@ async def try_trigger_fortune(avatar: Avatar) -> list[Event]: __all__ = [ "try_trigger_fortune", + "get_cultivation_exp_reward", ] diff --git a/src/classes/misfortune.py b/src/classes/misfortune.py new file mode 100644 index 0000000..f228436 --- /dev/null +++ b/src/classes/misfortune.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import random +from enum import Enum +from typing import Optional + +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.fortune import get_cultivation_exp_reward + +class MisfortuneKind(Enum): + """霉运类型""" + LOSS_SPIRIT_STONE = "loss_spirit_stone" # 破财 + INJURY = "injury" # 受伤 + CULTIVATION_BACKLASH = "backlash" # 修为倒退 + + +MF_LOSS_SPIRIT_STONE_THEMES: list[str] = [ + "遭遇扒手", + "误买假货", + "遭人勒索", + "洞府失窃", + "赌石惨败", + "投资失败", +] + +MF_INJURY_THEMES: list[str] = [ + "修炼岔气", + "出门摔伤", + "妖兽偷袭", + "仇家闷棍", + "误触机关", + "天降横祸", +] + +MF_BACKLASH_THEMES: list[str] = [ + "心魔滋生", + "灵气逆行", + "感悟错乱", + "急火攻心", +] + + +def _choose_misfortune_kind(avatar: Avatar) -> Optional[MisfortuneKind]: + """选择霉运类型""" + candidates = [] + + # 破财:必须有灵石 + if avatar.magic_stone.value > 0: + candidates.append(MisfortuneKind.LOSS_SPIRIT_STONE) + + # 受伤:任何人都可以受伤 + candidates.append(MisfortuneKind.INJURY) + + # 修为倒退:只有修炼者(有修为且未满?)或者任何人都可以? + # 简单处理:任何人都可以走火入魔 + candidates.append(MisfortuneKind.CULTIVATION_BACKLASH) + + if not candidates: + return None + + return random.choice(candidates) + + +def _pick_misfortune_theme(kind: MisfortuneKind) -> str: + if kind == MisfortuneKind.LOSS_SPIRIT_STONE: + return random.choice(MF_LOSS_SPIRIT_STONE_THEMES) + elif kind == MisfortuneKind.INJURY: + return random.choice(MF_INJURY_THEMES) + elif kind == MisfortuneKind.CULTIVATION_BACKLASH: + return random.choice(MF_BACKLASH_THEMES) + return "" + + +async def try_trigger_misfortune(avatar: Avatar) -> list[Event]: + """ + 触发霉运 + 规则: + - 概率:config + effects + - 类型:破财、受伤、修为倒退 + - 破财:随机数,不超过总量 + - 受伤:扣减HP,可能致死(由simulator结算) + - 修为倒退:扣减经验,不降级(经验值可为负?)-> 此处逻辑:扣减当前经验,最小为0 + """ + base_prob = float(getattr(CONFIG.game, "misfortune_probability", 0.0)) + extra_prob = float(avatar.effects.get("extra_misfortune_probability", 0.0)) + prob = base_prob + extra_prob + if prob <= 0.0: + return [] + + if random.random() >= prob: + return [] + + kind = _choose_misfortune_kind(avatar) + if kind is None: + return [] + + theme = _pick_misfortune_theme(kind) + res_text: str = "" + + if kind == MisfortuneKind.LOSS_SPIRIT_STONE: + # 破财:随机数,不超过总量 + max_loss = avatar.magic_stone.value + # 设定一个随机范围,例如 10~500,但受 max_loss 限制 + # 或者完全随机 + loss = random.randint(50, 300) + loss = min(loss, max_loss) + avatar.magic_stone.value -= loss + res_text = f"{avatar.name} 损失灵石 {loss} 枚" + + elif kind == MisfortuneKind.INJURY: + # 受伤:扣减HP + # 扣减量:最大生命值的 10%~80% + 固定值 + max_hp = avatar.hp.max + ratio = random.uniform(0.1, 0.8) + damage = int(max_hp * ratio) + random.randint(10, 50) + + avatar.hp.cur -= damage + # 注意:这里可能扣成负数,simulator 会在 _phase_resolve_death 中处理 + res_text = f"{avatar.name} 受到伤害 {damage} 点,剩余HP {avatar.hp.cur}/{max_hp}" + + elif kind == MisfortuneKind.CULTIVATION_BACKLASH: + # 修为倒退 + # 扣减量:100~500 + loss = random.randint(100, 500) + + # 确保不扣到负数(或者允许负数?通常经验不为负) + # 这里只扣减当前经验,不掉级 + current_exp = avatar.cultivation_progress.exp + actual_loss = min(current_exp, loss) + avatar.cultivation_progress.exp -= actual_loss + + res_text = f"{avatar.name} 修为倒退" + + # 生成故事 + 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=[avatar.id], is_major=True) + + story = await StoryTeller.tell_story( + event_text, res_text, avatar, + prompt=story_prompt, + allow_relation_changes=False + ) + story_event = Event(month_at_finish, story, related_avatars=[avatar.id], is_story=True) + + return [base_event, story_event] + diff --git a/src/sim/simulator.py b/src/sim/simulator.py index aac6343..dfada3b 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -13,6 +13,7 @@ from src.classes.name import get_random_name from src.utils.config import CONFIG from src.run.log import get_logger from src.classes.fortune import try_trigger_fortune +from src.classes.misfortune import try_trigger_misfortune from src.classes.celestial_phenomenon import get_random_celestial_phenomenon from src.classes.long_term_objective import process_avatar_long_term_objective from src.classes.death import handle_death @@ -203,9 +204,10 @@ class Simulator: for avatar in living_avatars: avatar.update_time_effect() - # 使用 gather 并行触发奇遇 - tasks = [try_trigger_fortune(avatar) for avatar in living_avatars] - results = await asyncio.gather(*tasks) + # 使用 gather 并行触发奇遇和霉运 + tasks_fortune = [try_trigger_fortune(avatar) for avatar in living_avatars] + tasks_misfortune = [try_trigger_misfortune(avatar) for avatar in living_avatars] + results = await asyncio.gather(*(tasks_fortune + tasks_misfortune)) events.extend([e for res in results if res for e in res]) diff --git a/static/config.yml b/static/config.yml index 9de6201..979e74c 100644 --- a/static/config.yml +++ b/static/config.yml @@ -25,6 +25,7 @@ game: sect_num: 3 # init_npc_num大于sect_num时,会随机选择sect_num个宗门 npc_birth_rate_per_month: 0 fortune_probability: 0.005 + misfortune_probability: 0.005 df: ids_separator: ";"