Files
cultivation-world-simulator/web/src/stores/world.ts
2025-11-22 17:26:54 +08:00

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
};
});