diff --git a/src/server/main.py b/src/server/main.py index 9e3e7f1..96f99bd 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -118,6 +118,34 @@ def serialize_events_for_client(events: List[Event]) -> List[dict]: }) return serialized +def serialize_phenomenon(phenomenon) -> Optional[dict]: + """序列化天地灵机对象""" + if not phenomenon: + return None + + # 安全地获取 rarity.name + rarity_str = "N" + if hasattr(phenomenon, "rarity") and phenomenon.rarity: + # 检查 rarity 是否是 Enum (RarityLevel) + if hasattr(phenomenon.rarity, "name"): + rarity_str = phenomenon.rarity.name + # 检查 rarity 是否是 Rarity dataclass (包含 level 字段) + elif hasattr(phenomenon.rarity, "level") and hasattr(phenomenon.rarity.level, "name"): + rarity_str = phenomenon.rarity.level.name + + # 生成效果描述 + from src.utils.effect_desc import format_effects_to_text + effect_desc = format_effects_to_text(phenomenon.effects) if hasattr(phenomenon, "effects") else "" + + return { + "id": phenomenon.id, + "name": phenomenon.name, + "desc": phenomenon.desc, + "rarity": rarity_str, + "duration_years": phenomenon.duration_years, + "effect_desc": effect_desc + } + def init_game(): """初始化游戏世界,逻辑复用自 src/run/run.py""" print("正在初始化游戏世界...") @@ -215,7 +243,8 @@ async def game_loop(): "year": int(world.month_stamp.get_year()), "month": world.month_stamp.get_month().value, "events": serialize_events_for_client(events), - "avatars": avatar_updates + "avatars": avatar_updates, + "phenomenon": serialize_phenomenon(world.current_phenomenon) } await manager.broadcast(state) except Exception as e: @@ -394,6 +423,7 @@ def get_state(): "avatar_count": len(world.avatar_manager.avatars), "avatars": av_list, "events": recent_events, + "phenomenon": serialize_phenomenon(world.current_phenomenon), "is_paused": game_instance.get("is_paused", False) } diff --git a/static/config.yml b/static/config.yml index a03698f..2a814d3 100644 --- a/static/config.yml +++ b/static/config.yml @@ -18,8 +18,8 @@ ai: max_parse_retries: 3 game: - init_npc_num: 12 - sect_num: 3 # init_npc_num大于sect_num时,会随机选择sect_num个宗门 + init_npc_num: 6 + sect_num: 2 # init_npc_num大于sect_num时,会随机选择sect_num个宗门 npc_birth_rate_per_month: 0.01 fortune_probability: 0.005 diff --git a/web/src/components/layout/StatusBar.vue b/web/src/components/layout/StatusBar.vue index 2ee0c7c..d57442f 100644 --- a/web/src/components/layout/StatusBar.vue +++ b/web/src/components/layout/StatusBar.vue @@ -1,7 +1,8 @@ @@ -28,6 +41,35 @@ onUnmounted(() => { {{ store.year }}年 {{ store.month }}月 + + + + | + + + + [{{ store.currentPhenomenon.name }}] + + + + + {{ store.currentPhenomenon.name }} + {{ store.currentPhenomenon.rarity }} + + {{ store.currentPhenomenon.desc }} + + + + 效果: + {{ store.currentPhenomenon.effect_desc }} + + + + 持续 {{ store.currentPhenomenon.duration_years }} 年 + + + + 肥桥今天吃什么的 { margin-right: 8px; } +.center { + display: flex; + align-items: center; + gap: 10px; +} + +.phenomenon { + display: flex; + align-items: center; + gap: 10px; +} + +.divider { + color: #444; +} + +.phenomenon-name { + cursor: help; + font-weight: bold; +} + +.phenomenon-card { + padding: 4px 0; +} + +.p-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-weight: bold; + font-size: 15px; + border-bottom: 1px solid #333; + padding-bottom: 4px; +} + +.p-rarity { + font-size: 12px; + opacity: 0.8; + border: 1px solid currentColor; + padding: 0 4px; + border-radius: 2px; +} + +.p-desc { + font-size: 13px; + color: #ddd; + line-height: 1.5; + margin-bottom: 8px; +} + +/* 统一的效果块样式 */ +.effect-block { + background: rgba(255, 255, 255, 0.05); + border: 1px solid #444; + border-radius: 4px; + padding: 8px 10px; + margin: 8px 0; +} + +.effect-label { + font-size: 12px; + color: #888; + margin-bottom: 4px; +} + +.effect-content { + font-size: 13px; + color: #fadb14; /* 亮黄色,匹配游戏常见的高亮色 */ + font-weight: 500; + line-height: 1.5; + white-space: pre-wrap; +} + +.p-duration { + font-size: 12px; + color: #888; + text-align: right; +} + .status-dot { display: inline-block; width: 6px; diff --git a/web/src/stores/world.ts b/web/src/stores/world.ts index 3f24e00..30c5761 100644 --- a/web/src/stores/world.ts +++ b/web/src/stores/world.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { ref, shallowRef, computed } from 'vue'; -import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary } from '../types/core'; +import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary, CelestialPhenomenon } from '../types/core'; import type { TickPayloadDTO, InitialStateDTO, MapResponseDTO } from '../types/api'; import { gameApi } from '../api/game'; @@ -20,6 +20,8 @@ export const useWorldStore = defineStore('world', () => { const regions = shallowRef>(new Map()); const isLoaded = ref(false); + + const currentPhenomenon = ref(null); // --- Getters --- @@ -118,6 +120,9 @@ export const useWorldStore = defineStore('world', () => { if (payload.avatars) updateAvatars(payload.avatars); if (payload.events) addEvents(payload.events); + if (payload.phenomenon !== undefined) { + currentPhenomenon.value = payload.phenomenon; + } } async function initialize() { @@ -145,6 +150,9 @@ export const useWorldStore = defineStore('world', () => { // 3. Set Events (Initial state might have history?) events.value = []; if (stateRes.events) addEvents(stateRes.events); + + // 4. Set Phenomenon + currentPhenomenon.value = stateRes.phenomenon || null; isLoaded.value = true; } catch (e) { @@ -158,6 +166,7 @@ export const useWorldStore = defineStore('world', () => { avatars.value = new Map(); events.value = []; isLoaded.value = false; + currentPhenomenon.value = null; } return { @@ -169,10 +178,10 @@ export const useWorldStore = defineStore('world', () => { mapData, regions, isLoaded, + currentPhenomenon, initialize, handleTick, reset }; }); - diff --git a/web/src/types/api.ts b/web/src/types/api.ts index 469bda8..b4c0c4a 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -3,7 +3,7 @@ * 这些类型严格对应后端接口返回的 JSON 结构。 */ -import type { MapMatrix } from './core'; +import type { MapMatrix, CelestialPhenomenon } from './core'; // --- 通用响应 --- @@ -29,6 +29,7 @@ export interface InitialStateDTO { pic_id?: number; }>; events?: unknown[]; + phenomenon?: CelestialPhenomenon | null; } export interface TickPayloadDTO { @@ -37,6 +38,7 @@ export interface TickPayloadDTO { month: number; avatars?: Array>; events?: unknown[]; + phenomenon?: CelestialPhenomenon | null; } export interface MapResponseDTO { @@ -64,4 +66,3 @@ export interface SaveFileDTO { game_time: string; version: string; } - diff --git a/web/src/types/core.ts b/web/src/types/core.ts index 404f671..76e4094 100644 --- a/web/src/types/core.ts +++ b/web/src/types/core.ts @@ -117,6 +117,17 @@ export interface RegionDetail extends EntityBase { plants: EffectEntity[]; } +// --- 天地灵机 --- + +export interface CelestialPhenomenon { + id: number; + name: string; + desc: string; + rarity: string; + duration_years?: number; + effect_desc?: string; +} + // --- 事件 (Events) --- export interface GameEvent { @@ -140,4 +151,3 @@ export type HoverSegment = { }; export type HoverLine = HoverSegment[]; -