diff --git a/src/classes/action/__init__.py b/src/classes/action/__init__.py index 63e6c0e..22c4f4f 100644 --- a/src/classes/action/__init__.py +++ b/src/classes/action/__init__.py @@ -26,7 +26,7 @@ from .play import Play from .hunt import Hunt from .harvest import Harvest from .sold import SellItems -from .battle import Battle +from .attack import Attack from .plunder_mortals import PlunderMortals from .help_mortals import HelpMortals from .devour_mortals import DevourMortals @@ -34,6 +34,7 @@ from .self_heal import SelfHeal from .catch import Catch from .nurture_weapon import NurtureWeapon from .switch_weapon import SwitchWeapon +from .assassinate import Assassinate # 注册到 ActionRegistry(标注是否为实际可执行动作) register_action(actual=False)(Action) @@ -56,7 +57,7 @@ register_action(actual=True)(Play) register_action(actual=True)(Hunt) register_action(actual=True)(Harvest) register_action(actual=True)(SellItems) -register_action(actual=False)(Battle) +register_action(actual=False)(Attack) register_action(actual=True)(PlunderMortals) register_action(actual=True)(HelpMortals) register_action(actual=True)(DevourMortals) @@ -64,6 +65,7 @@ register_action(actual=True)(SelfHeal) register_action(actual=True)(Catch) register_action(actual=True)(NurtureWeapon) register_action(actual=True)(SwitchWeapon) +register_action(actual=True)(Assassinate) # Talk 已移动到 mutual_action 模块,在那里注册 __all__ = [ @@ -89,7 +91,7 @@ __all__ = [ "Hunt", "Harvest", "SellItems", - "Battle", + "Attack", "PlunderMortals", "HelpMortals", "DevourMortals", @@ -97,6 +99,7 @@ __all__ = [ "Catch", "NurtureWeapon", "SwitchWeapon", + "Assassinate", # Talk 已移动到 mutual_action 模块 ] diff --git a/src/classes/action/assassinate.py b/src/classes/action/assassinate.py new file mode 100644 index 0000000..918fa13 --- /dev/null +++ b/src/classes/action/assassinate.py @@ -0,0 +1,159 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +import random + +from src.classes.action import InstantAction +from src.classes.action.cooldown import cooldown_action +from src.classes.event import Event +from src.classes.battle import decide_battle, get_assassination_success_rate +from src.classes.story_teller import StoryTeller +from src.classes.normalize import normalize_avatar_name +from src.classes.death import handle_death +from src.classes.kill_and_grab import kill_and_grab + +if TYPE_CHECKING: + from src.classes.avatar import Avatar + + +@cooldown_action +class Assassinate(InstantAction): + COMMENT = "暗杀目标,失败则变为战斗" + DOABLES_REQUIREMENTS = "任何时候都可以执行;需要冷却" + PARAMS = {"avatar_name": "AvatarName"} + ACTION_CD_MONTHS = 12 + + # 成功与失败的提示词 + STORY_PROMPT_SUCCESS = ( + "这是关于一次成功的暗杀。不需要描写战斗过程,重点描写刺客如何潜伏、接近,以及最后那一击的致命与悄无声息。" + "目标甚至没有反应过来就已经陨落。" + ) + STORY_PROMPT_FAIL = ( + "这是关于一次失败的暗杀。刺客试图暗杀目标,但被目标敏锐地察觉了。" + "双方随后爆发了激烈的正面冲突。" + "不要出现具体血量数值。" + ) + + # 暗杀是大事(长期记忆) + IS_MAJOR: bool = True + + def _get_target(self, avatar_name: str) -> Avatar | None: + normalized_name = normalize_avatar_name(avatar_name) + for v in self.world.avatar_manager.avatars.values(): + if v.name == normalized_name: + return v + return None + + def _execute(self, avatar_name: str) -> None: + target = self._get_target(avatar_name) + if target is None: + return + + # 判定暗杀是否成功 + success_rate = get_assassination_success_rate(self.avatar, target) + is_success = random.random() < success_rate + + self._is_assassinate_success = is_success + + if is_success: + # 暗杀成功,目标直接死亡 + target.hp.current = 0 + self._last_result = None # 不需要战斗结果 + else: + # 暗杀失败,转入正常战斗 + winner, loser, loser_damage, winner_damage = decide_battle(self.avatar, target) + # 应用双方伤害 + loser.hp.reduce(loser_damage) + winner.hp.reduce(winner_damage) + + # 增加熟练度(既然打起来了) + proficiency_gain = random.uniform(1.0, 3.0) + self.avatar.increase_weapon_proficiency(proficiency_gain) + target.increase_weapon_proficiency(proficiency_gain) + + self._last_result = (winner, loser, loser_damage, winner_damage) + + def can_start(self, avatar_name: str | None = None) -> tuple[bool, str]: + # 注意:cooldown_action 装饰器会覆盖这个方法并在调用此方法前检查 CD + if avatar_name is None: + return False, "缺少参数 avatar_name" + ok = self._get_target(avatar_name) is not None + return (ok, "" if ok else "目标不存在") + + def start(self, avatar_name: str) -> Event: + target = self._get_target(avatar_name) + target_name = target.name if target is not None else avatar_name + + event = Event(self.world.month_stamp, f"{self.avatar.name} 潜伏在阴影中,试图暗杀 {target_name}...", related_avatars=[self.avatar.id, target.id] if target else [self.avatar.id], is_major=True) + self._start_event_content = event.content + return event + + async def finish(self, avatar_name: str) -> list[Event]: + target = self._get_target(avatar_name) + if target is None: + return [] + + rel_ids = [self.avatar.id, target.id] + + if getattr(self, '_is_assassinate_success', False): + # --- 暗杀成功 --- + result_text = f"{self.avatar.name} 暗杀成功!{target.name} 在毫无防备中陨落。" + + # 杀人夺宝 + loot_text = await kill_and_grab(self.avatar, target) + result_text += loot_text + + result_event = Event(self.world.month_stamp, result_text, related_avatars=rel_ids, is_major=True) + + # 生成故事 + story = await StoryTeller.tell_story( + self._start_event_content, + result_event.content, + self.avatar, + target, + prompt=self.STORY_PROMPT_SUCCESS, + allow_relation_changes=True + ) + story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True) + + # 死亡清理 + handle_death(self.world, target) + + return [result_event, story_event] + + else: + # --- 暗杀失败,转入战斗 --- + res = getattr(self, '_last_result', None) + if not (isinstance(res, tuple) and len(res) == 4): + return [] + + winner, loser, loser_damage, winner_damage = res + + is_fatal = loser.hp <= 0 + + prefix = f"暗杀失败!双方爆发激战。" + + if is_fatal: + result_text = f"{prefix} {winner.name} 最终战胜并斩杀了 {loser.name} (伤害 {loser_damage})。" + loot_text = await kill_and_grab(winner, loser) + result_text += loot_text + else: + result_text = f"{prefix} {winner.name} 战胜了 {loser.name},造成 {loser_damage} 点伤害,自身受损 {winner_damage} 点。" + + result_event = Event(self.world.month_stamp, result_text, related_avatars=rel_ids, is_major=True) + + # 生成故事 + story = await StoryTeller.tell_story( + self._start_event_content, + result_event.content, + self.avatar, + target, + prompt=self.STORY_PROMPT_FAIL, + allow_relation_changes=True + ) + story_event = Event(self.world.month_stamp, story, related_avatars=rel_ids, is_story=True) + + if is_fatal: + handle_death(self.world, loser) + + return [result_event, story_event] + diff --git a/src/classes/action/battle.py b/src/classes/action/attack.py similarity index 98% rename from src/classes/action/battle.py rename to src/classes/action/attack.py index d893763..d4b65dc 100644 --- a/src/classes/action/battle.py +++ b/src/classes/action/attack.py @@ -9,8 +9,8 @@ from src.classes.normalize import normalize_avatar_name from src.classes.death import handle_death from src.classes.kill_and_grab import kill_and_grab -class Battle(InstantAction): - COMMENT = "与目标进行对战,判定胜负" +class Attack(InstantAction): + COMMENT = "攻击目标,进行对战" DOABLES_REQUIREMENTS = "任何时候都可以执行" PARAMS = {"avatar_name": "AvatarName"} # 提供用于故事生成的提示词:不出现血量/伤害等数值描述 diff --git a/src/classes/action/escape.py b/src/classes/action/escape.py index 4ff11e8..f179889 100644 --- a/src/classes/action/escape.py +++ b/src/classes/action/escape.py @@ -11,7 +11,7 @@ class Escape(InstantAction): """ 逃离:尝试从对方身边脱离(有成功率)。 成功:抢占并进入 MoveAwayFromAvatar(6个月)。 - 失败:抢占并进入 Battle。 + 失败:抢占并进入 Attack。 """ COMMENT = "逃离对方(基于成功率判定)" @@ -57,7 +57,7 @@ class Escape(InstantAction): EventHelper.push_pair(start_event, initiator=self.avatar, target=target, to_sidebar_once=True) else: self._preempt_avatar(self.avatar) - self.avatar.load_decide_result_chain([("Battle", {"avatar_name": avatar_name})], self.avatar.thinking, "") + self.avatar.load_decide_result_chain([("Attack", {"avatar_name": avatar_name})], self.avatar.thinking, "") start_event = self.avatar.commit_next_plan() if start_event is not None: EventHelper.push_pair(start_event, initiator=self.avatar, target=target, to_sidebar_once=True) diff --git a/src/classes/avatar.py b/src/classes/avatar.py index 011de17..761f68d 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -508,7 +508,7 @@ class Avatar(AvatarSaveMixin, AvatarLoadMixin): params_for_finish = filter_kwargs_for_callable(action.finish, params) finish_events = await action.finish(**params_for_finish) # 仅当当前动作仍然是刚才执行的那个实例时才清空 - # 若在 step() 内部通过"抢占"机制切换了动作(如 Escape 失败立即切到 Battle),不要清空新动作 + # 若在 step() 内部通过"抢占"机制切换了动作(如 Escape 失败立即切到 Attack),不要清空新动作 if self.current_action is action_instance_before: self.current_action = None # 动作完成后,如果有待执行计划,立即提交下一个(支持同月链式执行) diff --git a/src/classes/battle.py b/src/classes/battle.py index 4d59e92..40c252e 100644 --- a/src/classes/battle.py +++ b/src/classes/battle.py @@ -185,4 +185,31 @@ def get_escape_success_rate(attacker: "Avatar", defender: "Avatar") -> float: """ base_rate = 0.1 bonus = float(defender.effects.get("extra_escape_success_rate", 0.0)) - return max(0.0, min(1.0, base_rate + bonus)) \ No newline at end of file + return max(0.0, min(1.0, base_rate + bonus)) + + +def get_assassination_success_rate(attacker: "Avatar", defender: "Avatar") -> float: + """ + 暗杀成功率: + - 基础 10% + - 同境界 10% + - 每高一个大境界 +5%,每低一个大境界 -5% + - 范围 [1%, 100%] + """ + from src.classes.cultivation import Realm + realm_order = { + Realm.Qi_Refinement: 1, + Realm.Foundation_Establishment: 2, + Realm.Core_Formation: 3, + Realm.Nascent_Soul: 4, + } + + base_rate = 0.10 + + attacker_rank = realm_order.get(attacker.cultivation_progress.realm, 1) + defender_rank = realm_order.get(defender.cultivation_progress.realm, 1) + + diff = attacker_rank - defender_rank + rate = base_rate + diff * 0.05 + + return max(0.01, min(1.0, rate)) diff --git a/src/classes/mutual_action/__init__.py b/src/classes/mutual_action/__init__.py index b3f126c..c9ad0de 100644 --- a/src/classes/mutual_action/__init__.py +++ b/src/classes/mutual_action/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from .mutual_action import MutualAction from .drive_away import DriveAway -from .attack import Attack +from .attack import MutualAttack from .conversation import Conversation from .dual_cultivation import DualCultivation from .talk import Talk @@ -14,7 +14,7 @@ from src.classes.action.registry import register_action __all__ = [ "MutualAction", "DriveAway", - "Attack", + "MutualAttack", "Conversation", "DualCultivation", "Talk", @@ -25,7 +25,7 @@ __all__ = [ # 注册 mutual actions(均为实际动作) register_action(actual=True)(DriveAway) -register_action(actual=True)(Attack) +register_action(actual=True)(MutualAttack) register_action(actual=True)(Conversation) register_action(actual=True)(DualCultivation) register_action(actual=True)(Talk) diff --git a/src/classes/mutual_action/attack.py b/src/classes/mutual_action/attack.py index 2a838da..d82317e 100644 --- a/src/classes/mutual_action/attack.py +++ b/src/classes/mutual_action/attack.py @@ -9,14 +9,14 @@ if TYPE_CHECKING: @cooldown_action -class Attack(MutualAction): +class MutualAttack(MutualAction): """攻击另一个NPC""" ACTION_NAME = "攻击" COMMENT = "对目标进行攻击。" DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} - FEEDBACK_ACTIONS = ["Escape", "Battle"] + FEEDBACK_ACTIONS = ["Escape", "Attack"] STORY_PROMPT: str = "" # 攻击冷却:避免同月连刷攻击 ACTION_CD_MONTHS: int = 3 @@ -35,7 +35,7 @@ class Attack(MutualAction): if fb == "Escape": params = {"avatar_name": self.avatar.name} self._set_target_immediate_action(target_avatar, fb, params) - elif fb == "Battle": + elif fb == "Attack": params = {"avatar_name": self.avatar.name} self._set_target_immediate_action(target_avatar, fb, params) diff --git a/src/classes/mutual_action/drive_away.py b/src/classes/mutual_action/drive_away.py index d055a57..0e327da 100644 --- a/src/classes/mutual_action/drive_away.py +++ b/src/classes/mutual_action/drive_away.py @@ -16,7 +16,7 @@ class DriveAway(MutualAction): COMMENT = "以武力威慑对方离开此地。" DOABLES_REQUIREMENTS = "目标在交互范围内;不能连续执行" PARAMS = {"target_avatar": "AvatarName"} - FEEDBACK_ACTIONS = ["MoveAwayFromRegion", "Battle"] + FEEDBACK_ACTIONS = ["MoveAwayFromRegion", "Attack"] STORY_PROMPT: str = "" # 驱赶冷却:避免反复驱赶刷屏 ACTION_CD_MONTHS: int = 3 @@ -34,7 +34,7 @@ class DriveAway(MutualAction): # 驱赶选择离开:必定成功,不涉及概率 params = {"region": self.avatar.tile.region.name} self._set_target_immediate_action(target_avatar, fb, params) - elif fb == "Battle": + elif fb == "Attack": params = {"avatar_name": self.avatar.name} self._set_target_immediate_action(target_avatar, fb, params) diff --git a/src/classes/mutual_action/mutual_action.py b/src/classes/mutual_action/mutual_action.py index 7359e74..ce272e3 100644 --- a/src/classes/mutual_action/mutual_action.py +++ b/src/classes/mutual_action/mutual_action.py @@ -43,7 +43,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin): "MoveAwayFromAvatar": "试图远离", "MoveAwayFromRegion": "试图离开区域", "Escape": "逃离", - "Battle": "战斗", + "Attack": "战斗", } # 若该互动动作可能生成小故事,可在子类中覆盖该提示词 STORY_PROMPT: str | None = None diff --git a/src/classes/world.py b/src/classes/world.py index bb12820..2c0ca20 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -30,6 +30,10 @@ class World(): static_info = self.static_info map_info = self.map.get_info(detailed=detailed) world_info = {**map_info, **static_info} + + if self.current_phenomenon: + world_info["天地灵机"] = f"【{self.current_phenomenon.name}】{self.current_phenomenon.desc}" + return world_info def get_avatars_in_same_region(self, avatar: "Avatar"): @@ -40,4 +44,4 @@ class World(): @property def static_info(self) -> dict: - return {"static_info": "这是一个修仙世界,修仙的境界有:练气、筑基、金丹、元婴。"} \ No newline at end of file + return {"世界描述": "这是一个修仙世界,修仙的境界有:练气、筑基、金丹、元婴。"} \ No newline at end of file diff --git a/src/server/main.py b/src/server/main.py index 06e4b77..e80cb35 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -30,6 +30,7 @@ from src.classes.cultivation import REALM_ORDER from src.classes.alignment import Alignment from src.classes.color import serialize_hover_lines from src.classes.event import Event +from src.classes.celestial_phenomenon import celestial_phenomena_by_id from src.classes.long_term_objective import set_user_long_term_objective, clear_user_long_term_objective from src.sim.save.save_game import save_game, list_saves from src.sim.load.load_game import load_game @@ -796,6 +797,39 @@ def get_avatar_list_simple(): result.sort(key=lambda x: x["name"]) return {"avatars": result} +@app.get("/api/meta/phenomena") +def get_phenomena_list(): + """获取所有可选的天地灵机列表""" + result = [] + # 按 ID 排序 + for p in sorted(celestial_phenomena_by_id.values(), key=lambda x: x.id): + result.append(serialize_phenomenon(p)) + return {"phenomena": result} + +class SetPhenomenonRequest(BaseModel): + id: int + +@app.post("/api/control/set_phenomenon") +def set_phenomenon(req: SetPhenomenonRequest): + world = game_instance.get("world") + if not world: + raise HTTPException(status_code=503, detail="World not initialized") + + p = celestial_phenomena_by_id.get(req.id) + if not p: + raise HTTPException(status_code=404, detail="Phenomenon not found") + + world.current_phenomenon = p + + # 重置计时器,使其从当前年份开始重新计算持续时间 + try: + current_year = int(world.month_stamp.get_year()) + world.phenomenon_start_year = current_year + except Exception: + pass + + return {"status": "ok", "message": f"Phenomenon set to {p.name}"} + @app.post("/api/action/create_avatar") def create_avatar(req: CreateAvatarRequest): """创建新角色""" diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 997fbcb..5fdef2e 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -172,50 +172,47 @@ class Simulator: - 生成世界事件记录天象变化 天象变化时机: - - 从游戏第二年(101年)开始 - - 每5年(或当前天象指定的持续时间)变化一次 + - 初始年份(如100年)1月立即开始第一个天象 + - 每N年(当前天象指定的持续时间)变化一次 """ events = [] current_year = self.world.month_stamp.get_year() current_month = self.world.month_stamp.get_month() - # 第一年(100年)不触发天象 - if current_year < 101: - return events + # 检查是否需要初始化或更新天象 + # 1. 如果没有天象 (初始化) + # 2. 如果有天象且到期 (每年一月检查) + should_update = False + is_init = False - # 初次运行:在101年1月设置初始天象 - if self.world.current_phenomenon is None and current_month == Month.JANUARY: + if self.world.current_phenomenon is None: + should_update = True + is_init = True + elif current_month == Month.JANUARY: + elapsed_years = current_year - self.world.phenomenon_start_year + if elapsed_years >= self.world.current_phenomenon.duration_years: + should_update = True + + if should_update: + 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 - # 生成世界事件(不绑定任何角色) + + desc = "" + if is_init: + desc = f"世界初开,天降异象!{new_phenomenon.name}:{new_phenomenon.desc}。" + else: + desc = f"{old_phenomenon.name}消散,天地异象再现!{new_phenomenon.name}:{new_phenomenon.desc}。" + event = Event( self.world.month_stamp, - f"天降异象!{new_phenomenon.name}:{new_phenomenon.desc}。", - related_avatars=None # 世界事件,不绑定角色 + 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 diff --git a/static/game_configs/celestial_phenomenon.csv b/static/game_configs/celestial_phenomenon.csv index 43fd875..867bff5 100644 --- a/static/game_configs/celestial_phenomenon.csv +++ b/static/game_configs/celestial_phenomenon.csv @@ -1,24 +1,23 @@ id,name,rarity,effects,desc,duration_years -1,紫气东来,R,{extra_cultivate_exp: 15},天降祥瑞,紫气弥漫东方,修士修炼速度大增,5 -2,金煞之年,R,"[{when: 'avatar.weapon.weapon_type.value in [""剑"", ""刀""]', extra_battle_strength_points: 5, extra_weapon_proficiency_gain: 0.3}, {extra_battle_strength_points: 3}]",金煞充盈天地肃杀,剑修刀修锋芒毕露,众修战力皆增,5 -3,木灵盛世,R,"{extra_harvest_items: 2, extra_hp_recovery_rate: 0.5}",木德滋养生机盎然,灵药遍地灵兽成群,疗伤复元如沐甘霖,5 -4,水德之纪,R,"{extra_cultivate_exp: 20, cultivate_duration_reduction: 0.2}",水行流转通达无碍,修炼如行云流水,悟法通神一日千里,5 -5,火劫时代,R,"{extra_battle_strength_points: 5, extra_max_lifespan: -50}",天火燃烧劫数降临,战力暴涨却损耗寿元,如烈火燃尽生机,5 -6,土厚之世,R,"{damage_reduction: 0.15, extra_max_hp: 150}",土德厚重载物无疆,根基稳固血气充盈,百邪难侵万法不破,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: 5, extra_fortune_probability: -0.005}",劫数降临戾气弥漫,修士战力暴涨却杀机四伏,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: 5, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: -5}]",魔气滔天邪道横行,正道式微难以抗衡,5 -13,正气浩然,R,"[{when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: 5, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.EVIL', extra_battle_strength_points: -0}]",浩然正气镇压邪祟,正道昌盛,5 -14,神兵出世,SR,"{extra_weapon_proficiency_gain: 1.0}",神兵有灵百兵齐鸣,温养进境一日千里,5 -15,阴阳交泰,R,"{extra_dual_cultivation_exp: 20}",阴阳交泰天地和合,双修之道水到渠成,5 -16,杀劫降临,SR,"{extra_battle_strength_points: 5, extra_fortune_probability: -0.005}",血光冲天杀机四伏,战斗凶险倍增,5 -17,太平盛世,R,"{extra_cultivate_exp: -10, extra_fortune_probability: 0.1}",天下太平万物安宁,修炼迟缓却机缘频生,5 -18,气运加身,R,"[{when: 'any(p.name == ""气运之子"" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.005}]",天命眷顾气运之子,修炼奇遇皆蒙福泽,5 -19,血月当空,SR,"{extra_battle_strength_points: 7, extra_cultivate_exp: -10}",血月高悬杀机暴涨,战斗狂热但修心不易,5 -20,飞升之门,SSR,"{extra_cultivate_exp: 30, extra_breakthrough_success_rate: 0.2}",天门大开飞升有望,巅峰修士得窥天道,7 -21,法则显化,SSR,"{extra_breakthrough_success_rate: 0.5}",天地法则显化于世,众修士感悟突破,3 -22,时空乱流,SSR,"{extra_fortune_probability: 0.005}",时空错乱奇遇频生,机缘无数,5 - +1,紫气东来,R,{extra_cultivate_exp: 15},天地灵气充沛,修士修行速度大增,修行欲望提高,5 +2,金煞之年,R,"{extra_battle_strength_points: 3}",金煞充盈天地肃杀,修士更大可能嗜血而相互攻伐,5 +3,木灵盛世,R,"{extra_harvest_items: 2, extra_hp_recovery_rate: 0.5}",木德滋养生机盎然,采集收获倍增且伤势恢复极快,宜四处搜罗天材地宝,5 +4,水德之纪,R,"{extra_cultivate_exp: 20, cultivate_duration_reduction: 0.2}",水行流转通达无碍,修炼效率与速度双重提升,正是闭关苦修的大好时机,5 +5,火劫时代,R,"{extra_battle_strength_points: 5, extra_max_lifespan: -50}",天火燃烧劫数降临,战力暴涨但寿元流逝,当速战速决以命搏天,5 +6,土厚之世,R,"{damage_reduction: 0.15, extra_max_hp: 150}",土德厚重载物无疆,身躯坚韧血气充盈,可无惧强敌正面争锋,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: 5, extra_fortune_probability: -0.005}",劫数降临戾气弥漫,虽战力激增但福缘浅薄,宜如履薄冰谨慎行事,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: 5, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: -5}]",魔气滔天邪道大昌,魔修如鱼得水,正道修士当避其锋芒潜心积淀,5 +13,正气浩然,R,"[{when: 'avatar.alignment == Alignment.GOOD', extra_battle_strength_points: 5, extra_cultivate_exp: 10}, {when: 'avatar.alignment == Alignment.EVIL', extra_battle_strength_points: -0}]",浩然正气镇压邪祟,正道修士如有神助,魔修宜韬光养晦莫触霉头,5 +14,神兵出世,SR,"{extra_weapon_proficiency_gain: 1.0}",神兵有灵百兵齐鸣,兵器熟练度提升极快,宜勤练武艺磨砺锋芒,5 +15,阴阳交泰,R,"{extra_dual_cultivation_exp: 20}",阴阳交泰天地和合,彼此互补进境神速,最宜寻觅道侣共参双修之乐,5 +16,杀劫降临,SR,"{extra_battle_strength_points: 5, extra_fortune_probability: -0.005}",血光冲天杀意难抑,虽杀伐之力大增但机缘断绝,宜主动出击以杀证道,5 +17,太平盛世,R,"{extra_cultivate_exp: -10, extra_fortune_probability: 0.1}",天下太平万物安宁,虽苦修进境缓慢但机缘频出,宜游历天下寻找奇遇,5 +18,气运加身,R,"[{when: 'any(p.name == ""气运之子"" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.005}]",天命眷顾气运所钟,天选之人万事皆顺,常人只可仰望不可强求,5 +19,血月当空,SR,"{extra_battle_strength_points: 7, extra_cultivate_exp: -10}",血月高悬人心躁动,战力虽强但静修困难,宜以战养战掠夺资源,5 +20,飞升之门,SSR,"{extra_cultivate_exp: 30, extra_breakthrough_success_rate: 0.2}",天门大开大道显现,感悟修行皆如有神助,正是冲击境界飞升成仙之时,7 +21,法则显化,SSR,"{extra_breakthrough_success_rate: 0.5}",天地法则清晰可触,瓶颈如纸一捅即破,万勿错过此突破天赐良机,3 +22,时空乱流,SSR,"{extra_fortune_probability: 0.005}",时空错乱异象频生,险地亦藏无上机缘,宜富贵险中求探索未知,5 diff --git a/web/src/api/game.ts b/web/src/api/game.ts index 51afad7..3f2e367 100644 --- a/web/src/api/game.ts +++ b/web/src/api/game.ts @@ -49,6 +49,15 @@ export interface CreateAvatarParams { appearance?: number; } +export interface PhenomenonDTO { + id: number; + name: string; + desc: string; + rarity: string; + duration_years: number; + effect_desc: string; +} + export const gameApi = { // --- World State --- @@ -64,6 +73,14 @@ export const gameApi = { return httpClient.get<{ males: number[]; females: number[] }>('/api/meta/avatars'); }, + fetchPhenomenaList() { + return httpClient.get<{ phenomena: PhenomenonDTO[] }>('/api/meta/phenomena'); + }, + + setPhenomenon(id: number) { + return httpClient.post('/api/control/set_phenomenon', { id }); + }, + // --- Information --- fetchHoverInfo(params: HoverParams) { diff --git a/web/src/components/layout/StatusBar.vue b/web/src/components/layout/StatusBar.vue index d57442f..96f99d8 100644 --- a/web/src/components/layout/StatusBar.vue +++ b/web/src/components/layout/StatusBar.vue @@ -2,10 +2,12 @@ import { useWorldStore } from '../../stores/world' import { gameSocket } from '../../api/socket' import { ref, onMounted, onUnmounted, computed } from 'vue' -import { NPopover } from 'naive-ui' +import { NPopover, NModal, NList, NListItem, NTag, NEmpty, useMessage } from 'naive-ui' const store = useWorldStore() +const message = useMessage() const isConnected = ref(false) +const showSelector = ref(false) // Update status locally since socket store is bare-bones let cleanup: (() => void) | undefined; @@ -23,14 +25,29 @@ onUnmounted(() => { const phenomenonColor = computed(() => { const p = store.currentPhenomenon; if (!p) return '#ccc'; - switch (p.rarity) { + return getRarityColor(p.rarity); +}) + +function getRarityColor(rarity: string) { + switch (rarity) { case 'N': return '#ccc'; case 'R': return '#4dabf7'; // Blue case 'SR': return '#a0d911'; // Lime case 'SSR': return '#fa8c16'; // Orange/Gold default: return '#ccc'; } -}) +} + +async function openPhenomenonSelector() { + showSelector.value = true; + await store.getPhenomenaList(); +} + +async function handleSelect(id: number, name: string) { + await store.changePhenomenon(id); + showSelector.value = false; + message.success(`天象已更易为:${name}`); +}