import { defineStore } from 'pinia'; import { ref, shallowRef, computed } from 'vue'; import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary, CelestialPhenomenon } from '../types/core'; import type { TickPayloadDTO, InitialStateDTO } from '../types/api'; import type { FetchEventsParams } from '../api/game'; import { gameApi } from '../api/game'; import { processNewEvents, mergeAndSortEvents } from '../utils/eventHelper'; export const useWorldStore = defineStore('world', () => { // --- State --- const year = ref(0); const month = ref(0); // 使用 shallowRef 存储大量数据以优化性能 // Key: Avatar ID const avatars = shallowRef>(new Map()); const events = shallowRef([]); // 分页状态 const eventsCursor = ref(null); const eventsHasMore = ref(false); const eventsLoading = ref(false); const eventsFilter = ref({}); const mapData = shallowRef([]); const regions = shallowRef>(new Map()); const isLoaded = ref(false); const frontendConfig = ref>({}); const currentPhenomenon = ref(null); const phenomenaList = shallowRef([]); // --- Getters --- const avatarList = computed(() => Array.from(avatars.value.values())); // --- Actions --- function setTime(y: number, m: number) { year.value = y; month.value = m; } function updateAvatars(list: Partial[]) { const next = new Map(avatars.value); let changed = false; for (const av of list) { if (!av.id) continue; const existing = next.get(av.id); if (existing) { // Merge next.set(av.id, { ...existing, ...av } as AvatarSummary); changed = true; } else { // New Avatar? Only insert if it has enough info (at least name) // This handles newly born avatars sent by backend if (av.name) { next.set(av.id, av as AvatarSummary); changed = true; } } // Else: ignore. Do NOT insert new avatars from tick updates unless they have full info. } if (changed) { avatars.value = next; } } function addEvents(rawEvents: any[]) { if (!rawEvents || rawEvents.length === 0) return; let newEvents = processNewEvents(rawEvents, year.value, month.value); // 根据当前筛选条件过滤(数据在 SQLite 中不会丢失) const filter = eventsFilter.value; if (filter.avatar_id) { newEvents = newEvents.filter(e => e.relatedAvatarIds?.includes(filter.avatar_id!) ); } else if (filter.avatar_id_1 && filter.avatar_id_2) { newEvents = newEvents.filter(e => e.relatedAvatarIds?.includes(filter.avatar_id_1!) && e.relatedAvatarIds?.includes(filter.avatar_id_2!) ); } if (newEvents.length === 0) return; // WebSocket 推送的新事件直接追加到末尾(最新事件在底部) // 使用 Set 去重(基于 id) const existingIds = new Set(events.value.map(e => e.id)); const uniqueNewEvents = newEvents.filter(e => !existingIds.has(e.id)); if (uniqueNewEvents.length > 0) { events.value = [...events.value, ...uniqueNewEvents]; } } function handleTick(payload: TickPayloadDTO) { if (!isLoaded.value) return; setTime(payload.year, payload.month); if (payload.avatars) updateAvatars(payload.avatars); if (payload.events) addEvents(payload.events); if (payload.phenomenon !== undefined) { currentPhenomenon.value = payload.phenomenon; } } function applyStateSnapshot(stateRes: InitialStateDTO) { setTime(stateRes.year, stateRes.month); const avatarMap = new Map(); if (stateRes.avatars) { stateRes.avatars.forEach(av => avatarMap.set(av.id, av)); } avatars.value = avatarMap; // 事件通过 resetEvents() 从分页 API 加载,这里只重置状态。 events.value = []; eventsCursor.value = null; eventsHasMore.value = false; eventsFilter.value = {}; currentPhenomenon.value = stateRes.phenomenon || null; isLoaded.value = true; } async function initialize() { try { const [stateRes, mapRes] = await Promise.all([ gameApi.fetchInitialState(), gameApi.fetchMap() ]); mapData.value = mapRes.data; if (mapRes.config) { frontendConfig.value = mapRes.config; } const regionMap = new Map(); mapRes.regions.forEach(r => regionMap.set(r.id, r)); regions.value = regionMap; applyStateSnapshot(stateRes); // 从分页 API 加载事件。 await resetEvents({}); } catch (e) { console.error('Failed to initialize world', e); } } async function fetchState() { try { const stateRes = await gameApi.fetchInitialState(); applyStateSnapshot(stateRes); } catch (e) { console.error('Failed to fetch state snapshot', e); } } function reset() { year.value = 0; month.value = 0; avatars.value = new Map(); events.value = []; eventsCursor.value = null; eventsHasMore.value = false; eventsFilter.value = {}; isLoaded.value = false; currentPhenomenon.value = null; } // --- 事件分页 --- async function loadEvents(filter: FetchEventsParams = {}, append = false) { if (eventsLoading.value) return; eventsLoading.value = true; try { const params: FetchEventsParams = { ...filter, limit: 100 }; if (append && eventsCursor.value) { params.cursor = eventsCursor.value; } const res = await gameApi.fetchEvents(params); // 转换为 GameEvent 格式 const newEvents: GameEvent[] = res.events.map(e => ({ id: e.id, text: e.text, content: e.content, year: e.year, month: e.month, monthStamp: e.month_stamp, relatedAvatarIds: e.related_avatar_ids, isMajor: e.is_major, isStory: e.is_story, })); // API 返回倒序(最新在前),反转成时间正序(最旧在前,最新在后) const sortedNewEvents = newEvents.reverse(); if (append) { // 加载更旧的事件,添加到顶部。 events.value = [...sortedNewEvents, ...events.value]; } else { // 切换筛选条件:直接用 API 数据替换,不做 merge。 // TODO: API 请求期间 WebSocket 推送的事件可能丢失,用户可手动刷新。 events.value = sortedNewEvents; eventsFilter.value = filter; } eventsCursor.value = res.next_cursor; eventsHasMore.value = res.has_more; } catch (e) { console.error('Failed to load events', e); } finally { eventsLoading.value = false; } } async function loadMoreEvents() { if (!eventsHasMore.value || eventsLoading.value) return; await loadEvents(eventsFilter.value, true); } async function resetEvents(filter: FetchEventsParams = {}) { eventsLoading.value = false; // 强制允许新请求,避免被旧请求阻塞。 eventsCursor.value = null; eventsHasMore.value = false; events.value = []; // 清空旧数据,避免筛选切换时显示残留。 eventsFilter.value = filter; // 立即更新筛选条件,让 addEvents 也能正确过滤。 await loadEvents(filter, false); } 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, avatars, avatarList, events, eventsCursor, eventsHasMore, eventsLoading, eventsFilter, mapData, regions, isLoaded, frontendConfig, currentPhenomenon, phenomenaList, // Functions. initialize, fetchState, handleTick, reset, loadEvents, loadMoreEvents, resetEvents, getPhenomenaList, changePhenomenon }; });