From 62fa84a809466ada341156ec15c92a2b76ac4be5 Mon Sep 17 00:00:00 2001 From: bridge Date: Thu, 13 Nov 2025 12:15:08 +0800 Subject: [PATCH] update celestial phenon --- src/classes/avatar.py | 4 + src/classes/celestial_phenomenon.py | 116 +++++++++++++++++++ src/classes/world.py | 7 +- src/front/app.py | 24 +++- src/front/events_panel.py | 48 +++++++- src/sim/load/load_game.py | 7 ++ src/sim/save/save_game.py | 5 +- src/sim/simulator.py | 63 +++++++++- static/game_configs/celestial_phenomenon.csv | 26 +++++ 9 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 src/classes/celestial_phenomenon.py create mode 100644 static/game_configs/celestial_phenomenon.csv diff --git a/src/classes/avatar.py b/src/classes/avatar.py index e32d926..df40720 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -180,6 +180,10 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): if self.spirit_animal is not None: evaluated = _evaluate_conditional_effect(self.spirit_animal.effects, self) merged = _merge_effects(merged, evaluated) + # 来自天地灵机(世界级buff/debuff) + if self.world.current_phenomenon is not None: + evaluated = _evaluate_conditional_effect(self.world.current_phenomenon.effects, self) + merged = _merge_effects(merged, evaluated) # 评估动态效果表达式:值以 "eval(...)" 形式给出 final: dict[str, object] = {} for k, v in merged.items(): diff --git a/src/classes/celestial_phenomenon.py b/src/classes/celestial_phenomenon.py new file mode 100644 index 0000000..d6bdf61 --- /dev/null +++ b/src/classes/celestial_phenomenon.py @@ -0,0 +1,116 @@ +""" +天地灵机系统 +============= + +天地灵机是影响整个修仙世界的天象异动,提供全局性的buff/debuff。 + +特点: +- 不绑定任何角色,属于世界事件 +- 定期变化(默认5年一次) +- 支持条件判断(如针对特定灵根、兵器类型等) +- 使用effect系统,与角色自身effects合并 + +扩展性: +- 未来可支持多天象并存(主天象+次天象) +- 未来可支持特殊事件触发天象变化(如飞升、大战等) +- 未来可支持地域性天象(只影响特定区域/宗门) +""" + +from __future__ import annotations + +import random +from dataclasses import dataclass +from typing import Optional + +from src.utils.df import game_configs +from src.classes.effect import load_effect_from_str +from src.classes.rarity import Rarity, get_rarity_from_str + + +@dataclass +class CelestialPhenomenon: + """ + 天地灵机(天象异动) + + 字段与 static/game_configs/celestial_phenomenon.csv 对应: + - id: 唯一标识符 + - name: 天象名称(修仙风格) + - rarity: 稀有度(N/R/SR/SSR),决定显示颜色和出现概率 + - effects: JSON格式的效果配置,支持条件判断 + - desc: 天象描述文字(用于UI显示和事件生成) + - duration_years: 持续年限(默认5年) + """ + id: int + name: str + rarity: Rarity + effects: dict[str, object] + desc: str + duration_years: int + + @property + def weight(self) -> float: + """根据稀有度获取出现概率权重""" + return self.rarity.weight + + def get_info(self) -> str: + """获取简略信息""" + return self.name + + def get_detailed_info(self) -> str: + """获取详细信息""" + return f"{self.name}({self.desc})" + + +def _load_celestial_phenomena() -> dict[int, CelestialPhenomenon]: + """从配表加载天地灵机数据""" + phenomena_by_id: dict[int, CelestialPhenomenon] = {} + + if "celestial_phenomenon" not in game_configs: + return phenomena_by_id + + phenomenon_df = game_configs["celestial_phenomenon"] + for _, row in phenomenon_df.iterrows(): + # 解析稀有度 + rarity_val = row.get("rarity", "N") + rarity_str = str(rarity_val).strip().upper() + rarity = get_rarity_from_str(rarity_str) if rarity_str and rarity_str != "NAN" else get_rarity_from_str("N") + + # 解析effects + raw_effects_val = row.get("effects", "") + effects = load_effect_from_str(raw_effects_val) + + # 解析持续年限(默认5年) + duration_years = int(row.get("duration_years", 5)) + + phenomenon = CelestialPhenomenon( + id=int(row["id"]), + name=str(row["name"]), + rarity=rarity, + effects=effects, + desc=str(row["desc"]), + duration_years=duration_years, + ) + phenomena_by_id[phenomenon.id] = phenomenon + + return phenomena_by_id + + +# 从配表加载天地灵机数据 +celestial_phenomena_by_id = _load_celestial_phenomena() + + +def get_random_celestial_phenomenon() -> Optional[CelestialPhenomenon]: + """ + 按权重随机选择一个天地灵机 + + Returns: + CelestialPhenomenon 或 None(如果没有可用的天象) + """ + if not celestial_phenomena_by_id: + return None + + phenomena = list(celestial_phenomena_by_id.values()) + weights = [p.weight for p in phenomena] + + return random.choices(phenomena, weights=weights, k=1)[0] + diff --git a/src/classes/world.py b/src/classes/world.py index ecaa77d..96f2ab2 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from src.classes.map import Map from src.classes.calendar import Year, Month, MonthStamp @@ -8,6 +8,7 @@ from src.classes.event_manager import EventManager if TYPE_CHECKING: from src.classes.avatar import Avatar + from src.classes.celestial_phenomenon import CelestialPhenomenon @dataclass @@ -17,6 +18,10 @@ class World(): avatar_manager: AvatarManager = field(default_factory=AvatarManager) # 全局事件管理器 event_manager: EventManager = field(default_factory=EventManager) + # 当前天地灵机(世界级buff/debuff) + current_phenomenon: Optional["CelestialPhenomenon"] = None + # 天地灵机开始年份(用于计算持续时间) + phenomenon_start_year: int = 0 def get_info(self, detailed: bool = False) -> dict: """ diff --git a/src/front/app.py b/src/front/app.py index dbfe005..af3e2a6 100644 --- a/src/front/app.py +++ b/src/front/app.py @@ -270,24 +270,37 @@ class Front: # 计算筛选后的事件 if self._sidebar_filter_avatar_id is None: events_to_draw: List[Event] = self.events + elif self._sidebar_filter_avatar_id == "__world_events__": + # 特殊筛选:仅显示世界事件(不绑定任何角色) + events_to_draw = [e for e in self.events if not getattr(e, "related_avatars", None)] else: aid = self._sidebar_filter_avatar_id events_to_draw = [e for e in self.events if getattr(e, "related_avatars", None) and (aid in e.related_avatars)] - # 构造下拉选项(第一个是所有人;其余为当前世界中的角色)- 带缓存 + # 构造下拉选项(第一个是所有;其余为当前世界中的角色)- 带缓存 options = self._get_sidebar_options_cached() - sel_label = "所有人" - if self._sidebar_filter_avatar_id is not None: + sel_label = "所有" + if self._sidebar_filter_avatar_id == "__world_events__": + sel_label = "世界事件" + elif self._sidebar_filter_avatar_id is not None: sel_avatar = self.world.avatar_manager.avatars.get(self._sidebar_filter_avatar_id) if sel_avatar is not None: sel_label = sel_avatar.name + # 获取天地灵机相关信息 + current_phenomenon = self.world.current_phenomenon + phenomenon_start_year = self.world.phenomenon_start_year if hasattr(self.world, 'phenomenon_start_year') else 0 + current_year = self.world.month_stamp.get_year() + sidebar_ui = draw_sidebar( pygame, self.screen, self.colors, self.sidebar_font, events_to_draw, self.world.map, self.tile_size, self.margin, self.sidebar_width, status_bar_height, filter_selected_label=sel_label, filter_is_open=self._sidebar_filter_open, filter_options=options, + current_phenomenon=current_phenomenon, + phenomenon_start_year=phenomenon_start_year, + current_year=current_year, ) # 保存供点击检测 self._sidebar_ui = sidebar_ui @@ -608,7 +621,10 @@ class Front: def _get_sidebar_options_cached(self) -> List[tuple[str, Optional[str]]]: if (not self._sidebar_options_dirty) and self._sidebar_options_cache is not None: return self._sidebar_options_cache - options: List[tuple[str, Optional[str]]] = [("所有人", None)] + options: List[tuple[str, Optional[str]]] = [ + ("所有", None), + ("世界事件", "__world_events__") + ] for avatar_id, avatar in self.world.avatar_manager.avatars.items(): options.append((avatar.name, avatar_id)) self._sidebar_options_cache = options diff --git a/src/front/events_panel.py b/src/front/events_panel.py index 35ceaa0..2774088 100644 --- a/src/front/events_panel.py +++ b/src/front/events_panel.py @@ -40,6 +40,9 @@ def draw_sidebar( filter_selected_label: str, filter_is_open: bool, filter_options: List[Tuple[str, Optional[str]]], + current_phenomenon = None, + phenomenon_start_year: int = 0, + current_year: int = 0, ) -> Dict[str, object]: map_px_w, _ = map_pixel_size(type("_W", (), {"map": world_map})(), tile_size) sidebar_x = map_px_w + margin * 2 @@ -54,19 +57,58 @@ def draw_sidebar( pygame_mod.draw.rect(screen, colors["sidebar_bg"], sidebar_rect) pygame_mod.draw.rect(screen, colors["sidebar_border"], sidebar_rect, 2) - # 下拉选择器:显示“所有人/某人”,放在最上方 + # 天地灵机显示区域(放在最上方) + content_start_y = sidebar_y + 10 + if current_phenomenon is not None: + phenomenon_margin_x = 10 + phenomenon_width = sidebar_width - 20 + phenomenon_x = sidebar_x + phenomenon_margin_x + phenomenon_y = content_start_y + + # 计算持续时间 + elapsed_years = current_year - phenomenon_start_year + remaining_years = max(0, current_phenomenon.duration_years - elapsed_years) + + # 天象名称(使用稀有度颜色) + rarity_color = current_phenomenon.rarity.color_rgb + name_surf = font.render(f"天象:{current_phenomenon.name}", True, rarity_color) + screen.blit(name_surf, (phenomenon_x, phenomenon_y)) + phenomenon_y += name_surf.get_height() + 4 + + # 描述文字(自动换行) + usable_width = phenomenon_width + desc_lines = _wrap_text_by_pixels(font, current_phenomenon.desc, usable_width) + for line in desc_lines: + line_surf = font.render(line, True, colors["event_text"]) + screen.blit(line_surf, (phenomenon_x, phenomenon_y)) + phenomenon_y += line_surf.get_height() + 2 + + # 剩余时间 + time_text = f"剩余:{remaining_years}年" + time_surf = font.render(time_text, True, colors["event_text"]) + screen.blit(time_surf, (phenomenon_x, phenomenon_y)) + phenomenon_y += time_surf.get_height() + 8 + + # 分隔线 + pygame_mod.draw.line(screen, colors["sidebar_border"], + (sidebar_x + 10, phenomenon_y), + (sidebar_x + sidebar_width - 10, phenomenon_y), 1) + + content_start_y = phenomenon_y + 10 + + # 下拉选择器:显示"所有人/某人",位于天地灵机下方 dropdown_margin_x = 10 dropdown_width = sidebar_width - 20 # 先用一个基准高度,确保点击区域更易操作 dropdown_height = 24 dropdown_x = sidebar_x + dropdown_margin_x - dropdown_y = sidebar_y + 10 + dropdown_y = content_start_y dropdown_rect = pygame_mod.Rect(dropdown_x, dropdown_y, dropdown_width, dropdown_height) # 填充底色并描边 pygame_mod.draw.rect(screen, colors["sidebar_bg"], dropdown_rect) pygame_mod.draw.rect(screen, colors["sidebar_border"], dropdown_rect, 1) # 选中项文本 - sel_text = filter_selected_label or "所有人" + sel_text = filter_selected_label or "所有" sel_surf = font.render(f"筛选:{sel_text}", True, colors["event_text"]) screen.blit(sel_surf, (dropdown_x + 6, dropdown_y + (dropdown_height - sel_surf.get_height()) // 2)) # 右侧箭头 diff --git a/src/sim/load/load_game.py b/src/sim/load/load_game.py index 6ce9ea8..da5c508 100644 --- a/src/sim/load/load_game.py +++ b/src/sim/load/load_game.py @@ -88,6 +88,13 @@ def load_game(save_path: Optional[Path] = None) -> Tuple["World", "Simulator", L # 重建World对象 world = World(map=game_map, month_stamp=month_stamp) + # 重建天地灵机 + from src.classes.celestial_phenomenon import celestial_phenomena_by_id + phenomenon_id = world_data.get("current_phenomenon_id") + if phenomenon_id is not None and phenomenon_id in celestial_phenomena_by_id: + world.current_phenomenon = celestial_phenomena_by_id[phenomenon_id] + world.phenomenon_start_year = world_data.get("phenomenon_start_year", 0) + # 获取本局启用的宗门 existed_sect_ids = world_data.get("existed_sect_ids", []) existed_sects = [sects_by_id[sid] for sid in existed_sect_ids if sid in sects_by_id] diff --git a/src/sim/save/save_game.py b/src/sim/save/save_game.py index 3ca7ad9..a3b1fb2 100644 --- a/src/sim/save/save_game.py +++ b/src/sim/save/save_game.py @@ -82,7 +82,10 @@ def save_game( # 构建世界数据 world_data = { "month_stamp": int(world.month_stamp), - "existed_sect_ids": [sect.id for sect in existed_sects] + "existed_sect_ids": [sect.id for sect in existed_sects], + # 天地灵机 + "current_phenomenon_id": world.current_phenomenon.id if world.current_phenomenon else None, + "phenomenon_start_year": world.phenomenon_start_year if hasattr(world, 'phenomenon_start_year') else 0, } # 保存所有Avatar(第一阶段:不含relations) diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 8c5a75f..0a2f878 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -12,6 +12,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.celestial_phenomenon import get_random_celestial_phenomenon class Simulator: def __init__(self, world: World): @@ -120,6 +121,61 @@ class Simulator: fortune_events = await try_trigger_fortune(avatar) events.extend(fortune_events) return events + + def _phase_update_celestial_phenomenon(self): + """ + 更新天地灵机: + - 检查当前天象是否到期 + - 如果到期,则随机选择新天象 + - 生成世界事件记录天象变化 + + 天象变化时机: + - 从游戏第二年(101年)开始 + - 每5年(或当前天象指定的持续时间)变化一次 + """ + events = [] + current_year = self.world.month_stamp.get_year() + current_month = self.world.month_stamp.get_month() + + # 第一年(100年)不触发天象 + if current_year < 101: + return events + + # 初次运行:在101年1月设置初始天象 + if self.world.current_phenomenon is None and current_month == Month.JANUARY: + new_phenomenon = get_random_celestial_phenomenon() + if new_phenomenon: + self.world.current_phenomenon = new_phenomenon + self.world.phenomenon_start_year = current_year + # 生成世界事件(不绑定任何角色) + event = Event( + self.world.month_stamp, + f"天降异象!{new_phenomenon.name}:{new_phenomenon.desc}。", + related_avatars=None # 世界事件,不绑定角色 + ) + events.append(event) + elif self.world.current_phenomenon is not None: + # 检查是否到期(每年一月检查) + if current_month == Month.JANUARY: + elapsed_years = current_year - self.world.phenomenon_start_year + if elapsed_years >= self.world.current_phenomenon.duration_years: + # 天象到期,更换新天象 + old_phenomenon = self.world.current_phenomenon + new_phenomenon = get_random_celestial_phenomenon() + + if new_phenomenon: + self.world.current_phenomenon = new_phenomenon + self.world.phenomenon_start_year = current_year + + # 生成天象变化事件 + event = Event( + self.world.month_stamp, + f"{old_phenomenon.name}消散,天地异象再现!{new_phenomenon.name}:{new_phenomenon.desc}。", + related_avatars=None # 世界事件 + ) + events.append(event) + + return events def _phase_log_events(self, events): """ @@ -158,14 +214,17 @@ class Simulator: # 6. 被动结算(时间效果+奇遇) events.extend(await self._phase_passive_effects()) - # 7. 日志 + # 7. 更新天地灵机 + events.extend(self._phase_update_celestial_phenomenon()) + + # 8. 日志 # 统一写入事件管理器 if hasattr(self.world, "event_manager") and self.world.event_manager is not None: for e in events: self.world.event_manager.add_event(e) self._phase_log_events(events) - # 8. 时间推进 + # 9. 时间推进 self.world.month_stamp = self.world.month_stamp + 1 diff --git a/static/game_configs/celestial_phenomenon.csv b/static/game_configs/celestial_phenomenon.csv new file mode 100644 index 0000000..4553ee7 --- /dev/null +++ b/static/game_configs/celestial_phenomenon.csv @@ -0,0 +1,26 @@ +id,name,rarity,effects,desc,duration_years +1,紫气东来,R,{extra_cultivate_exp: 15},天降祥瑞,紫气弥漫东方,修士修炼速度大增,5 +2,金行旺盛,R,"[{when: 'any(e.value == ""金"" for e in avatar.root.elements)', extra_battle_strength_points: 20, extra_cultivate_exp: 15}]",金行之力充盈天地,金灵根修士如虎添翼,5 +3,木气盎然,R,"[{when: 'any(e.value == ""木"" for e in avatar.root.elements)', extra_dual_cultivation_exp: 15, extra_max_hp: 100}]",木德之气滋养万物,木灵根修士生机勃勃,5 +4,水德之年,R,"[{when: 'any(e.value == ""水"" for e in avatar.root.elements)', extra_breakthrough_success_rate: 0.2, extra_cultivate_exp: 10}]",水行流转通达无碍,水灵根修士突破更易,5 +5,火运当空,R,"[{when: 'any(e.value == ""火"" for e in avatar.root.elements)', extra_battle_strength_points: 30, extra_cultivate_exp: -5}]",火势炽烈易生心魔,火灵根修士战力暴涨但修炼不稳,5 +6,土德载物,R,"[{when: 'any(e.value == ""土"" for e in avatar.root.elements)', extra_cultivate_exp: 20, extra_max_hp: 80}]",土德厚重承载万物,土灵根修士根基更稳,5 +7,五行逆乱,SR,"{extra_cultivate_exp: -10, extra_breakthrough_success_rate: 0.3}",五行失序天地大乱,修炼艰难却蕴含突破良机,5 +8,天道压制,SR,"[{when: 'avatar.cultivation_progress.realm.value >= 6', extra_cultivate_exp: -25}, {when: 'avatar.cultivation_progress.realm.value < 6', extra_cultivate_exp: 10}]",天道显化压制强者扶助弱者,天地趋于平衡,5 +9,劫数将至,SR,"{extra_battle_strength_points: 20, extra_fortune_probability: -0.05}",杀劫降临天地戾气弥漫,修士战力大增但凶险倍增,5 +10,灵气复苏,SSR,"{extra_cultivate_exp: 25, extra_breakthrough_success_rate: 0.1}",天地灵气井喷复苏,修士修炼如沐春风,5 +11,灵气枯竭,R,"{extra_cultivate_exp: -20}",灵气衰竭修炼艰难,万物萧索,5 +12,魔道兴盛,R,"[{when: 'avatar.alignment == Alignment.EVIL', extra_battle_strength_points: 30, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: -10}]",魔气滔天邪道大兴,正道受压,5 +13,正气浩然,R,"[{when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: 25, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.EVIL', extra_battle_strength_points: -10}]",浩然正气镇压邪祟,正道昌盛,5 +14,剑道当空,SR,"[{when: 'avatar.weapon.weapon_type.value == ""剑""', extra_battle_strength_points: 25, extra_weapon_proficiency_gain: 0.5}]",剑气纵横万里,剑修天下无敌,5 +15,枪芒破空,R,"[{when: 'avatar.weapon.weapon_type.value == ""枪""', extra_battle_strength_points: 25, extra_weapon_proficiency_gain: 0.5}]",枪出如龙破碎虚空,枪修威震天下,5 +16,神兵出世,SR,"{extra_weapon_proficiency_gain: 1.0}",神兵有灵百兵共鸣,温养兵器事半功倍,5 +17,双修之机,R,"{extra_dual_cultivation_exp: 20}",阴阳交泰天地和合,双修效率大增,5 +18,杀劫降临,SR,"{extra_battle_strength_points: 15, extra_fortune_probability: -0.1}",血光冲天杀机四伏,战斗凶险倍增,5 +19,太平盛世,R,"{extra_cultivate_exp: -10, extra_fortune_probability: 0.1}",天下太平岁月静好,修炼缓慢但奇遇频现,5 +20,天道垂青,R,"[{when: 'any(p.name == ""气运之子"" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.15}]",天道显化垂怜苍生,气运者更受眷顾,5 +21,血月当空,SR,"{extra_battle_strength_points: 20, extra_cultivate_exp: -10}",血月高悬杀机暴涨,战斗狂热但修心不易,5 +22,飞升之门,SSR,"{extra_cultivate_exp: 30, extra_breakthrough_success_rate: 0.2}",天门大开飞升有望,巅峰修士得窥天道,7 +23,法则显化,SSR,"{extra_breakthrough_success_rate: 0.5}",天地法则显化于世,众修士感悟突破,3 +24,时空乱流,SSR,"{extra_fortune_probability: 0.1}",时空错乱奇遇频生,机缘无数,5 +