update map
This commit is contained in:
295
web/src/components/SystemMenu.vue
Normal file
295
web/src/components/SystemMenu.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { gameApi, type SaveFile } from '../services/gameApi'
|
||||
import { useGameStore } from '../stores/game'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const store = useGameStore()
|
||||
const message = useMessage()
|
||||
|
||||
const activeTab = ref<'save' | 'load'>('load')
|
||||
const saves = ref<SaveFile[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchSaves() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await gameApi.getSaves()
|
||||
saves.value = res.saves
|
||||
} catch (e) {
|
||||
message.error('获取存档列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await gameApi.saveGame()
|
||||
message.success(`存档成功: ${res.filename}`)
|
||||
await fetchSaves() // 刷新列表
|
||||
} catch (e) {
|
||||
message.error('存档失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoad(filename: string) {
|
||||
if (!confirm(`确定要加载存档 ${filename} 吗?当前未保存的进度将丢失。`)) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await store.reloadGame(filename)
|
||||
message.success('读档成功')
|
||||
emit('close')
|
||||
} catch (e) {
|
||||
message.error('读档失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
fetchSaves()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="system-menu-overlay" @click.self="emit('close')">
|
||||
<div class="system-menu">
|
||||
<div class="menu-header">
|
||||
<h2>系统菜单</h2>
|
||||
<button class="close-btn" @click="emit('close')">×</button>
|
||||
</div>
|
||||
|
||||
<div class="menu-tabs">
|
||||
<button
|
||||
:class="{ active: activeTab === 'save' }"
|
||||
@click="activeTab = 'save'; fetchSaves()"
|
||||
>
|
||||
保存游戏
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: activeTab === 'load' }"
|
||||
@click="activeTab = 'load'; fetchSaves()"
|
||||
>
|
||||
加载游戏
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="menu-content">
|
||||
<div v-if="loading" class="loading">处理中...</div>
|
||||
|
||||
<div v-else-if="activeTab === 'save'" class="save-panel">
|
||||
<div class="new-save-card" @click="handleSave">
|
||||
<div class="icon">+</div>
|
||||
<div>新建存档</div>
|
||||
<div class="sub">点击创建一个新的存档文件</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="load-panel">
|
||||
<div v-if="saves.length === 0" class="empty">暂无存档</div>
|
||||
<div
|
||||
v-for="save in saves"
|
||||
:key="save.filename"
|
||||
class="save-item"
|
||||
@click="handleLoad(save.filename)"
|
||||
>
|
||||
<div class="save-info">
|
||||
<div class="save-time">{{ save.save_time }}</div>
|
||||
<div class="game-time">游戏时间: {{ save.game_time }}</div>
|
||||
<div class="filename">{{ save.filename }}</div>
|
||||
</div>
|
||||
<div class="load-btn">加载</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.system-menu-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.system-menu {
|
||||
background: #1a1a1a;
|
||||
width: 600px;
|
||||
height: 500px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.menu-tabs button {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: #222;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-tabs button:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.menu-tabs button.active {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border-bottom: 2px solid #4a9eff;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.save-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.new-save-card {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
border: 2px dashed #444;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.new-save-card:hover {
|
||||
border-color: #666;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.new-save-card .icon {
|
||||
font-size: 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.new-save-card .sub {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.save-item {
|
||||
background: #222;
|
||||
border: 1px solid #333;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.save-item:hover {
|
||||
background: #2a2a2a;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.save-info .save-time {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.save-info .game-time {
|
||||
color: #4a9eff;
|
||||
font-size: 13px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.save-info .filename {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.load-btn {
|
||||
background: #333;
|
||||
color: #ddd;
|
||||
border: 1px solid #444;
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Application } from 'vue3-pixi'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { useGameStore } from '../../stores/game' // 引入 store
|
||||
import Viewport from './Viewport.vue'
|
||||
import MapLayer from './MapLayer.vue'
|
||||
import EntityLayer from './EntityLayer.vue'
|
||||
@@ -11,6 +12,8 @@ const container = ref<HTMLElement>()
|
||||
const { width, height } = useElementSize(container)
|
||||
const { loadBaseTextures, isLoaded } = useTextures()
|
||||
|
||||
const store = useGameStore() // 使用 store
|
||||
|
||||
const mapSize = ref({ width: 2000, height: 2000 })
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -59,7 +62,16 @@ onMounted(() => {
|
||||
:world-width="mapSize.width"
|
||||
:world-height="mapSize.height"
|
||||
>
|
||||
<MapLayer @mapLoaded="onMapLoaded" @regionSelected="handleRegionSelected" />
|
||||
<!--
|
||||
使用 store.worldVersion 作为 key
|
||||
当读档时,MapLayer 会被重新创建,从而重新加载地图数据
|
||||
但 Application 和 WebGL 上下文保持不变,避免崩溃
|
||||
-->
|
||||
<MapLayer
|
||||
:key="store.worldVersion"
|
||||
@mapLoaded="onMapLoaded"
|
||||
@regionSelected="handleRegionSelected"
|
||||
/>
|
||||
<EntityLayer @avatarSelected="handleAvatarSelected" />
|
||||
</Viewport>
|
||||
</Application>
|
||||
|
||||
@@ -61,6 +61,8 @@ async function renderMap() {
|
||||
const sprite = new Sprite(tex)
|
||||
sprite.x = x * TILE_SIZE
|
||||
sprite.y = y * TILE_SIZE
|
||||
// 开启像素取整,消除 Tile 之间的黑边缝隙
|
||||
sprite.roundPixels = true
|
||||
|
||||
if (['SECT', 'CITY', 'CAVE', 'RUINS'].includes(type)) {
|
||||
sprite.width = TILE_SIZE * 2
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ref } from 'vue'
|
||||
import { Assets, Texture } from 'pixi.js'
|
||||
import { Assets, Texture, TextureStyle } from 'pixi.js'
|
||||
|
||||
// 设置全局纹理缩放模式为 nearest (像素风)
|
||||
TextureStyle.defaultOptions.scaleMode = 'nearest'
|
||||
|
||||
// 全局纹理缓存,避免重复加载
|
||||
const textures = ref<Record<string, Texture>>({})
|
||||
|
||||
Reference in New Issue
Block a user