update celestial phenon
This commit is contained in:
@@ -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 模块
|
||||
]
|
||||
|
||||
|
||||
159
src/classes/action/assassinate.py
Normal file
159
src/classes/action/assassinate.py
Normal 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]
|
||||
|
||||
@@ -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"}
|
||||
# 提供用于故事生成的提示词:不出现血量/伤害等数值描述
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
# 动作完成后,如果有待执行计划,立即提交下一个(支持同月链式执行)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class MutualAction(DefineAction, LLMAction, TargetingMixin):
|
||||
"MoveAwayFromAvatar": "试图远离",
|
||||
"MoveAwayFromRegion": "试图离开区域",
|
||||
"Escape": "逃离",
|
||||
"Battle": "战斗",
|
||||
"Attack": "战斗",
|
||||
}
|
||||
# 若该互动动作可能生成小故事,可在子类中覆盖该提示词
|
||||
STORY_PROMPT: str | None = None
|
||||
|
||||
@@ -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 {"世界描述": "这是一个修仙世界,修仙的境界有:练气、筑基、金丹、元婴。"}
|
||||
@@ -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):
|
||||
"""创建新角色"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user