179 lines
4.6 KiB
TypeScript
179 lines
4.6 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import { ref, shallowRef, computed } from 'vue';
|
|
import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary } from '../types/core';
|
|
import type { TickPayloadDTO, InitialStateDTO, MapResponseDTO } from '../types/api';
|
|
import { gameApi } from '../api/game';
|
|
|
|
export const useWorldStore = defineStore('world', () => {
|
|
// --- State ---
|
|
|
|
const year = ref(0);
|
|
const month = ref(0);
|
|
|
|
// 使用 shallowRef 存储大量数据以优化性能
|
|
// Key: Avatar ID
|
|
const avatars = shallowRef<Map<string, AvatarSummary>>(new Map());
|
|
|
|
const events = shallowRef<GameEvent[]>([]);
|
|
|
|
const mapData = shallowRef<MapMatrix>([]);
|
|
const regions = shallowRef<Map<string | number, RegionSummary>>(new Map());
|
|
|
|
const isLoaded = ref(false);
|
|
|
|
// --- 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<AvatarSummary>[]) {
|
|
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;
|
|
|
|
// 转换 DTO -> Domain
|
|
const newEvents: GameEvent[] = rawEvents.map(e => ({
|
|
id: e.id,
|
|
text: e.text,
|
|
content: e.content,
|
|
year: e.year ?? year.value,
|
|
month: e.month ?? month.value,
|
|
timestamp: (e.year ?? year.value) * 12 + (e.month ?? month.value),
|
|
relatedAvatarIds: e.related_avatar_ids || [],
|
|
isMajor: e.is_major,
|
|
isStory: e.is_story
|
|
}));
|
|
|
|
// 排序并保留最新的 N 条
|
|
const MAX_EVENTS = 300;
|
|
const combined = [...newEvents, ...events.value];
|
|
combined.sort((a, b) => b.timestamp - a.timestamp); // 降序
|
|
|
|
events.value = combined.slice(0, MAX_EVENTS);
|
|
}
|
|
|
|
function handleTick(payload: TickPayloadDTO) {
|
|
if (!isLoaded.value) return;
|
|
|
|
setTime(payload.year, payload.month);
|
|
|
|
// 检查并处理死亡事件,移除已死亡的角色
|
|
if (payload.events && Array.isArray(payload.events)) {
|
|
const deathEvents = payload.events.filter((e: any) => {
|
|
const c = e.content || '';
|
|
return c.includes('身亡') || c.includes('老死');
|
|
});
|
|
|
|
if (deathEvents.length > 0) {
|
|
const next = new Map(avatars.value);
|
|
let changed = false;
|
|
|
|
for (const de of deathEvents) {
|
|
if (de.related_avatar_ids && Array.isArray(de.related_avatar_ids)) {
|
|
for (const id of de.related_avatar_ids) {
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
avatars.value = next;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (payload.avatars) updateAvatars(payload.avatars);
|
|
if (payload.events) addEvents(payload.events);
|
|
}
|
|
|
|
async function initialize() {
|
|
try {
|
|
const [stateRes, mapRes] = await Promise.all([
|
|
gameApi.fetchInitialState(),
|
|
gameApi.fetchMap()
|
|
]);
|
|
|
|
// 1. Set Map
|
|
mapData.value = mapRes.data;
|
|
const regionMap = new Map();
|
|
mapRes.regions.forEach(r => regionMap.set(r.id, r));
|
|
regions.value = regionMap;
|
|
|
|
// 2. Set State
|
|
setTime(stateRes.year, stateRes.month);
|
|
|
|
const avatarMap = new Map();
|
|
if (stateRes.avatars) {
|
|
stateRes.avatars.forEach(av => avatarMap.set(av.id, av));
|
|
}
|
|
avatars.value = avatarMap;
|
|
|
|
// 3. Set Events (Initial state might have history?)
|
|
events.value = [];
|
|
if (stateRes.events) addEvents(stateRes.events);
|
|
|
|
isLoaded.value = true;
|
|
} catch (e) {
|
|
console.error('Failed to initialize world', e);
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
year.value = 0;
|
|
month.value = 0;
|
|
avatars.value = new Map();
|
|
events.value = [];
|
|
isLoaded.value = false;
|
|
}
|
|
|
|
return {
|
|
year,
|
|
month,
|
|
avatars,
|
|
avatarList,
|
|
events,
|
|
mapData,
|
|
regions,
|
|
isLoaded,
|
|
|
|
initialize,
|
|
handleTick,
|
|
reset
|
|
};
|
|
});
|
|
|