refactor frontend (not done)
This commit is contained in:
160
web/src/composables/useGameControl.ts
Normal file
160
web/src/composables/useGameControl.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { ref, watch, onMounted, type Ref } from 'vue'
|
||||
import { llmApi } from '@/api'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
import { message } from '@/utils/discreteApi'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
export function useGameControl(gameInitialized: Ref<boolean>) {
|
||||
const uiStore = useUiStore()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const { isManualPaused } = storeToRefs(systemStore)
|
||||
const showMenu = ref(false)
|
||||
const menuDefaultTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm' | 'start'>('load')
|
||||
const canCloseMenu = ref(true)
|
||||
|
||||
// 监听菜单状态和手动暂停状态,控制游戏暂停/继续
|
||||
watch([showMenu, isManualPaused], ([menuVisible, manualPaused]) => {
|
||||
// 只在游戏已准备好时控制暂停
|
||||
if (!gameInitialized.value) return
|
||||
|
||||
if (menuVisible || manualPaused) {
|
||||
// 如果不是因为菜单打开而暂停(即用户手动暂停),则不需额外操作,因为状态已经在 store 里了
|
||||
// 但如果是菜单打开导致需要暂停,我们需要调用 pause
|
||||
if (menuVisible && !manualPaused) {
|
||||
systemStore.pause().catch(console.error)
|
||||
} else if (!menuVisible && !manualPaused) {
|
||||
// 菜单关闭且非手动暂停 -> 恢复
|
||||
systemStore.resume().catch(console.error)
|
||||
} else if (manualPaused) {
|
||||
// 只要是手动暂停,就确保是暂停状态
|
||||
systemStore.pause().catch(console.error)
|
||||
}
|
||||
} else {
|
||||
systemStore.resume().catch(console.error)
|
||||
}
|
||||
})
|
||||
|
||||
// 优化:简化 Watch 逻辑
|
||||
// 核心规则:菜单打开 OR 手动暂停 => 必须暂停
|
||||
// 菜单关闭 AND 手动播放 => 恢复
|
||||
watch([showMenu], ([menuVisible]) => {
|
||||
if (!gameInitialized.value) return
|
||||
if (menuVisible) {
|
||||
systemStore.pause().catch(console.error)
|
||||
} else {
|
||||
// 关闭菜单时,如果不是手动暂停状态,则恢复
|
||||
if (!isManualPaused.value) {
|
||||
systemStore.resume().catch(console.error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Watch 手动暂停状态的变化,同步到后端
|
||||
watch(isManualPaused, (val) => {
|
||||
if (!gameInitialized.value) return
|
||||
if (val) systemStore.pause().catch(console.error)
|
||||
else if (!showMenu.value) systemStore.resume().catch(console.error)
|
||||
})
|
||||
|
||||
// 快捷键处理
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
if (uiStore.selectedTarget) {
|
||||
uiStore.clearSelection()
|
||||
} else {
|
||||
if (showMenu.value) {
|
||||
// 如果菜单打开,尝试关闭(如果允许)
|
||||
if (canCloseMenu.value) {
|
||||
showMenu.value = false
|
||||
}
|
||||
} else {
|
||||
// 打开菜单
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'load'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LLM 相关控制逻辑
|
||||
async function performStartupCheck() {
|
||||
try {
|
||||
const res = await llmApi.fetchStatus()
|
||||
|
||||
if (!res.configured) {
|
||||
// 未配置 -> 强制进入 LLM 配置,禁止关闭
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'llm'
|
||||
canCloseMenu.value = false
|
||||
message.warning('检测到 LLM 未配置,请先完成设置')
|
||||
} else {
|
||||
// 已配置 -> 验证连通性
|
||||
try {
|
||||
const configRes = await llmApi.fetchConfig()
|
||||
await llmApi.testConnection(configRes)
|
||||
|
||||
// 测试通过 -> 允许进入开始游戏
|
||||
menuDefaultTab.value = 'start'
|
||||
canCloseMenu.value = true
|
||||
showMenu.value = true
|
||||
} catch (connErr) {
|
||||
// 连接失败 -> 强制进入配置
|
||||
console.error('LLM Connection check failed:', connErr)
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'llm'
|
||||
canCloseMenu.value = false
|
||||
message.error('LLM 连接测试失败,请重新配置')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to check LLM status:', e)
|
||||
// Fallback
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'llm'
|
||||
canCloseMenu.value = false
|
||||
message.error('无法获取系统状态')
|
||||
}
|
||||
}
|
||||
|
||||
function handleLLMReady() {
|
||||
canCloseMenu.value = true
|
||||
menuDefaultTab.value = 'start'
|
||||
message.success('LLM 配置成功,请开始游戏')
|
||||
}
|
||||
|
||||
function handleMenuClose() {
|
||||
if (canCloseMenu.value) {
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleManualPause() {
|
||||
systemStore.togglePause()
|
||||
}
|
||||
|
||||
function openLLMConfig() {
|
||||
menuDefaultTab.value = 'llm'
|
||||
showMenu.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 暴露给全局以便 socket store 可以调用
|
||||
;(window as any).__openLLMConfig = openLLMConfig
|
||||
})
|
||||
|
||||
return {
|
||||
showMenu,
|
||||
isManualPaused,
|
||||
menuDefaultTab,
|
||||
canCloseMenu,
|
||||
|
||||
handleKeydown,
|
||||
performStartupCheck,
|
||||
handleLLMReady,
|
||||
handleMenuClose,
|
||||
toggleManualPause,
|
||||
openLLMConfig
|
||||
}
|
||||
}
|
||||
109
web/src/composables/useGameInit.ts
Normal file
109
web/src/composables/useGameInit.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useSystemStore } from '@/stores/system'
|
||||
import { useWorldStore } from '@/stores/world'
|
||||
import { useSocketStore } from '@/stores/socket'
|
||||
import { GAME_PHASES } from '@/constants/game'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
interface UseGameInitOptions {
|
||||
onIdle?: () => void
|
||||
}
|
||||
|
||||
export function useGameInit(options: UseGameInitOptions = {}) {
|
||||
const systemStore = useSystemStore()
|
||||
const worldStore = useWorldStore()
|
||||
const socketStore = useSocketStore()
|
||||
|
||||
const { initStatus, isInitialized, isLoading } = storeToRefs(systemStore)
|
||||
|
||||
// 内部变量
|
||||
const mapPreloaded = ref(false)
|
||||
const avatarsPreloaded = ref(false)
|
||||
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// Methods
|
||||
async function initializeGame() {
|
||||
if (isInitialized.value) {
|
||||
// 重新加载存档时,重新初始化
|
||||
worldStore.reset()
|
||||
}
|
||||
|
||||
// 初始化 Socket 连接
|
||||
if (!socketStore.isConnected) {
|
||||
socketStore.init()
|
||||
}
|
||||
|
||||
// 初始化世界状态
|
||||
await worldStore.initialize()
|
||||
|
||||
systemStore.setInitialized(true)
|
||||
console.log('[GameInit] Game initialized.')
|
||||
}
|
||||
|
||||
async function pollInitStatus() {
|
||||
try {
|
||||
const prevStatus = initStatus.value?.status
|
||||
const res = await systemStore.fetchInitStatus()
|
||||
|
||||
if (!res) return
|
||||
|
||||
// Idle check
|
||||
if (res.status === 'idle' && prevStatus !== 'idle') {
|
||||
options.onIdle?.()
|
||||
}
|
||||
|
||||
// 提前加载地图
|
||||
if (!mapPreloaded.value && GAME_PHASES.MAP_READY.includes(res.phase_name as any)) {
|
||||
mapPreloaded.value = true
|
||||
worldStore.preloadMap()
|
||||
}
|
||||
|
||||
// 提前加载角色
|
||||
if (!avatarsPreloaded.value && GAME_PHASES.AVATAR_READY.includes(res.phase_name as any)) {
|
||||
avatarsPreloaded.value = true
|
||||
worldStore.preloadAvatars()
|
||||
}
|
||||
|
||||
// 状态跃迁:非 Ready -> Ready
|
||||
if (prevStatus !== 'ready' && res.status === 'ready') {
|
||||
await initializeGame()
|
||||
stopPolling()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch init status:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
pollInitStatus()
|
||||
pollInterval = setInterval(pollInitStatus, 1000)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval)
|
||||
pollInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startPolling()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
socketStore.disconnect()
|
||||
})
|
||||
|
||||
return {
|
||||
initStatus,
|
||||
gameInitialized: isInitialized, // Alias for compatibility
|
||||
showLoading: isLoading,
|
||||
mapPreloaded,
|
||||
avatarsPreloaded,
|
||||
initializeGame,
|
||||
startPolling,
|
||||
stopPolling
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user