update celestial phenon

This commit is contained in:
bridge
2025-11-29 13:30:06 +08:00
parent 10dba3c418
commit d7549d7d32
17 changed files with 440 additions and 78 deletions

View File

@@ -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 模块
]

View File

@@ -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]

View File

@@ -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"}
# 提供用于故事生成的提示词:不出现血量/伤害等数值描述

View File

@@ -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)

View File

@@ -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
# 动作完成后,如果有待执行计划,立即提交下一个(支持同月链式执行)

View File

@@ -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))
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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -43,7 +43,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
"MoveAwayFromAvatar": "试图远离",
"MoveAwayFromRegion": "试图离开区域",
"Escape": "逃离",
"Battle": "战斗",
"Attack": "战斗",
}
# 若该互动动作可能生成小故事,可在子类中覆盖该提示词
STORY_PROMPT: str | None = None

View File

@@ -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": "这是一个修仙世界,修仙的境界有:练气、筑基、金丹、元婴。"}
return {"世界描述": "这是一个修仙世界,修仙的境界有:练气、筑基、金丹、元婴。"}

View File

@@ -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):
"""创建新角色"""

View File

@@ -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

View File

@@ -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
1 id name rarity effects desc duration_years
2 1 紫气东来 R {extra_cultivate_exp: 15} 天降祥瑞,紫气弥漫东方,修士修炼速度大增 天地灵气充沛,修士修行速度大增,修行欲望提高 5
3 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}] {extra_battle_strength_points: 3} 金煞充盈天地肃杀,剑修刀修锋芒毕露,众修战力皆增 金煞充盈天地肃杀,修士更大可能嗜血而相互攻伐 5
4 3 木灵盛世 R {extra_harvest_items: 2, extra_hp_recovery_rate: 0.5} 木德滋养生机盎然,灵药遍地灵兽成群,疗伤复元如沐甘霖 木德滋养生机盎然,采集收获倍增且伤势恢复极快,宜四处搜罗天材地宝 5
5 4 水德之纪 R {extra_cultivate_exp: 20, cultivate_duration_reduction: 0.2} 水行流转通达无碍,修炼如行云流水,悟法通神一日千里 水行流转通达无碍,修炼效率与速度双重提升,正是闭关苦修的大好时机 5
6 5 火劫时代 R {extra_battle_strength_points: 5, extra_max_lifespan: -50} 天火燃烧劫数降临,战力暴涨却损耗寿元,如烈火燃尽生机 天火燃烧劫数降临,战力暴涨但寿元流逝,当速战速决以命搏天 5
7 6 土厚之世 R {damage_reduction: 0.15, extra_max_hp: 150} 土德厚重载物无疆,根基稳固血气充盈,百邪难侵万法不破 土德厚重载物无疆,身躯坚韧血气充盈,可无惧强敌正面争锋 5
8 7 五行逆乱 SR {extra_cultivate_exp: -10, extra_breakthrough_success_rate: 0.3} 五行失序天地大乱,修炼艰难却蕴含突破良机 五行失序天地大乱,常规修炼事倍功半,然瓶颈松动宜尝试突破 5
9 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
10 9 劫数将至 SR {extra_battle_strength_points: 5, extra_fortune_probability: -0.005} 劫数降临戾气弥漫,修士战力暴涨却杀机四伏 劫数降临戾气弥漫,虽战力激增但福缘浅薄,宜如履薄冰谨慎行事 5
11 10 灵气复苏 SSR {extra_cultivate_exp: 25, extra_breakthrough_success_rate: 0.1} 天地灵气井喷复苏,修士修炼如沐春风 天地灵气井喷复苏,修为进境一日千里,万物竞发宜全力精进 5
12 11 灵气枯竭 R {extra_cultivate_exp: -20} 灵气枯竭末法将至,修炼如逆水行舟举步维艰 灵气枯竭末法将至,修炼如逆水行舟,宜节约资源稳固道心 5
13 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
14 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
15 14 神兵出世 SR {extra_weapon_proficiency_gain: 1.0} 神兵有灵百兵齐鸣,温养进境一日千里 神兵有灵百兵齐鸣,兵器熟练度提升极快,宜勤练武艺磨砺锋芒 5
16 15 阴阳交泰 R {extra_dual_cultivation_exp: 20} 阴阳交泰天地和合,双修之道水到渠成 阴阳交泰天地和合,彼此互补进境神速,最宜寻觅道侣共参双修之乐 5
17 16 杀劫降临 SR {extra_battle_strength_points: 5, extra_fortune_probability: -0.005} 血光冲天杀机四伏,战斗凶险倍增 血光冲天杀意难抑,虽杀伐之力大增但机缘断绝,宜主动出击以杀证道 5
18 17 太平盛世 R {extra_cultivate_exp: -10, extra_fortune_probability: 0.1} 天下太平万物安宁,修炼迟缓却机缘频生 天下太平万物安宁,虽苦修进境缓慢但机缘频出,宜游历天下寻找奇遇 5
19 18 气运加身 R [{when: 'any(p.name == "气运之子" for p in avatar.personas)', extra_cultivate_exp: 25, extra_fortune_probability: 0.005}] 天命眷顾气运之子,修炼奇遇皆蒙福泽 天命眷顾气运所钟,天选之人万事皆顺,常人只可仰望不可强求 5
20 19 血月当空 SR {extra_battle_strength_points: 7, extra_cultivate_exp: -10} 血月高悬杀机暴涨,战斗狂热但修心不易 血月高悬人心躁动,战力虽强但静修困难,宜以战养战掠夺资源 5
21 20 飞升之门 SSR {extra_cultivate_exp: 30, extra_breakthrough_success_rate: 0.2} 天门大开飞升有望,巅峰修士得窥天道 天门大开大道显现,感悟修行皆如有神助,正是冲击境界飞升成仙之时 7
22 21 法则显化 SSR {extra_breakthrough_success_rate: 0.5} 天地法则显化于世,众修士感悟突破 天地法则清晰可触,瓶颈如纸一捅即破,万勿错过此突破天赐良机 3
23 22 时空乱流 SSR {extra_fortune_probability: 0.005} 时空错乱奇遇频生,机缘无数 时空错乱异象频生,险地亦藏无上机缘,宜富贵险中求探索未知 5

View File

@@ -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) {

View File

@@ -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}`);
}
</script>
<template>
@@ -47,7 +64,11 @@ const phenomenonColor = computed(() => {
<span class="divider">|</span>
<n-popover trigger="hover" placement="bottom" style="max-width: 300px;">
<template #trigger>
<span class="phenomenon-name" :style="{ color: phenomenonColor }">
<span
class="phenomenon-name"
:style="{ color: phenomenonColor }"
@click="openPhenomenonSelector"
>
[{{ store.currentPhenomenon.name }}]
</span>
</template>
@@ -67,10 +88,40 @@ const phenomenonColor = computed(() => {
<div class="p-duration" v-if="store.currentPhenomenon.duration_years">
持续 {{ store.currentPhenomenon.duration_years }}
</div>
<div class="click-tip">点击可更易天象</div>
</div>
</n-popover>
</div>
</div>
<!-- 天象选择器 Modal -->
<n-modal
v-model:show="showSelector"
preset="card"
title="天道干涉:更易天象"
style="width: 700px; max-height: 80vh; overflow-y: auto;"
>
<n-list hoverable clickable>
<n-list-item v-for="p in store.phenomenaList" :key="p.id" @click="handleSelect(p.id, p.name)">
<div class="list-item-content">
<div class="item-left">
<div class="item-name" :style="{ color: getRarityColor(p.rarity) }">
{{ p.name }}
<n-tag size="small" :bordered="false" :color="{ color: 'rgba(255,255,255,0.1)', textColor: getRarityColor(p.rarity) }">
{{ p.rarity }}
</n-tag>
</div>
<div class="item-desc">{{ p.desc }}</div>
</div>
<div class="item-right">
<div class="item-effect" v-if="p.effect_desc">{{ p.effect_desc }}</div>
</div>
</div>
</n-list-item>
<n-empty v-if="store.phenomenaList.length === 0" description="暂无天象数据" />
</n-list>
</n-modal>
<div class="author">
肥桥今天吃什么的<a
class="author-link"
@@ -129,8 +180,14 @@ const phenomenonColor = computed(() => {
}
.phenomenon-name {
cursor: help;
cursor: pointer;
font-weight: bold;
transition: opacity 0.2s;
}
.phenomenon-name:hover {
opacity: 0.8;
text-decoration: underline;
}
.phenomenon-card {
@@ -192,6 +249,45 @@ const phenomenonColor = computed(() => {
text-align: right;
}
.click-tip {
font-size: 10px;
color: #666;
text-align: center;
margin-top: 8px;
border-top: 1px dashed #333;
padding-top: 4px;
}
.list-item-content {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
}
.item-name {
font-weight: bold;
font-size: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.item-desc {
color: #aaa;
font-size: 13px;
}
.item-effect {
font-size: 12px;
color: #e6a23c; /* Warning color */
background: rgba(230, 162, 60, 0.1);
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
margin-top: 4px;
}
.status-dot {
display: inline-block;
width: 6px;

View File

@@ -22,6 +22,7 @@ export const useWorldStore = defineStore('world', () => {
const isLoaded = ref(false);
const currentPhenomenon = ref<CelestialPhenomenon | null>(null);
const phenomenaList = shallowRef<CelestialPhenomenon[]>([]);
// --- Getters ---
@@ -199,6 +200,28 @@ export const useWorldStore = defineStore('world', () => {
currentPhenomenon.value = null;
}
async function getPhenomenaList() {
if (phenomenaList.value.length > 0) return phenomenaList.value;
try {
const res = await gameApi.fetchPhenomenaList();
// The API returns DTOs which match CelestialPhenomenon structure enough for frontend display
phenomenaList.value = res.phenomena as CelestialPhenomenon[];
return phenomenaList.value;
} catch (e) {
console.error(e);
return [];
}
}
async function changePhenomenon(id: number) {
await gameApi.setPhenomenon(id);
// 乐观更新:直接从列表里找到并设置,不等下一次 tick
const p = phenomenaList.value.find(item => item.id === id);
if (p) {
currentPhenomenon.value = p;
}
}
return {
year,
month,
@@ -209,10 +232,13 @@ export const useWorldStore = defineStore('world', () => {
regions,
isLoaded,
currentPhenomenon,
phenomenaList,
initialize,
fetchState,
handleTick,
reset
reset,
getPhenomenaList,
changePhenomenon
};
});