-
+
@@ -50,6 +97,7 @@ function handleSelection(target: { type: 'avatar' | 'region'; id: string; name?:
background: #000;
color: #eee;
overflow: hidden;
+ position: relative;
}
.main-content {
@@ -66,6 +114,28 @@ function handleSelection(target: { type: 'avatar' | 'region'; id: string; name?:
overflow: hidden;
}
+.menu-toggle {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 100;
+ background: rgba(0,0,0,0.5);
+ border: 1px solid #444;
+ color: #ddd;
+ width: 40px;
+ height: 40px;
+ border-radius: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.menu-toggle:hover {
+ background: rgba(0,0,0,0.8);
+ border-color: #666;
+}
+
.sidebar {
width: 400px;
background: #181818;
diff --git a/web/src/components/SystemMenu.vue b/web/src/components/SystemMenu.vue
new file mode 100644
index 0000000..ef00ae4
--- /dev/null
+++ b/web/src/components/SystemMenu.vue
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
diff --git a/web/src/components/game/GameCanvas.vue b/web/src/components/game/GameCanvas.vue
index 9aca2e0..f598930 100644
--- a/web/src/components/game/GameCanvas.vue
+++ b/web/src/components/game/GameCanvas.vue
@@ -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
()
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"
>
-
+
+
diff --git a/web/src/components/game/MapLayer.vue b/web/src/components/game/MapLayer.vue
index 534cf9d..0fc1172 100644
--- a/web/src/components/game/MapLayer.vue
+++ b/web/src/components/game/MapLayer.vue
@@ -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
diff --git a/web/src/components/game/composables/useTextures.ts b/web/src/components/game/composables/useTextures.ts
index b1acc2f..a7eae9e 100644
--- a/web/src/components/game/composables/useTextures.ts
+++ b/web/src/components/game/composables/useTextures.ts
@@ -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>({})
diff --git a/web/src/services/gameApi.ts b/web/src/services/gameApi.ts
index fc327d1..132e55d 100644
--- a/web/src/services/gameApi.ts
+++ b/web/src/services/gameApi.ts
@@ -14,6 +14,13 @@ function buildHoverQuery(target: HoverTarget) {
return `/api/hover?${query.toString()}`
}
+export interface SaveFile {
+ filename: string
+ save_time: string
+ game_time: string
+ version: string
+}
+
export const gameApi = {
getInitialState() {
return apiGet('/api/state')
@@ -38,5 +45,33 @@ export const gameApi = {
return apiPost<{ status: string; message: string }>('/api/action/clear_long_term_objective', {
avatar_id: avatarId
})
+ },
+
+ // --- 游戏控制 API ---
+
+ pauseGame() {
+ return apiPost<{ status: string; message: string }>('/api/control/pause', {})
+ },
+
+ resumeGame() {
+ return apiPost<{ status: string; message: string }>('/api/control/resume', {})
+ },
+
+ // --- 存读档 API ---
+
+ getSaves() {
+ return apiGet<{ saves: SaveFile[] }>('/api/saves')
+ },
+
+ saveGame(filename?: string) {
+ return apiPost<{ status: string; filename: string }>('/api/game/save', {
+ filename
+ })
+ },
+
+ loadGame(filename: string) {
+ return apiPost<{ status: string; message: string }>('/api/game/load', {
+ filename
+ })
}
}
diff --git a/web/src/stores/game.ts b/web/src/stores/game.ts
index a0c6efa..6c77440 100644
--- a/web/src/stores/game.ts
+++ b/web/src/stores/game.ts
@@ -44,6 +44,9 @@ export const useGameStore = defineStore('game', () => {
const infoLoading = ref(false)
const infoError = ref(null)
const hoverCache = new Map()
+
+ // 新增:用于标记世界是否被重置(读档)
+ const worldVersion = ref(0)
const avatarList = computed(() => Object.values(avatars.value))
@@ -105,6 +108,9 @@ export const useGameStore = defineStore('game', () => {
})
avatars.value = nextAvatars
}
+
+ // 重置事件列表,而不是追加,因为是全新状态
+ events.value = []
appendEvents(data.events)
} catch (error) {
console.error('Fetch State Error', error)
@@ -164,6 +170,26 @@ export const useGameStore = defineStore('game', () => {
}
}
+ async function reloadGame(filename: string) {
+ // 1. 调用加载接口
+ await gameApi.loadGame(filename)
+
+ // 2. 清空前端状态
+ avatars.value = {}
+ events.value = []
+ hoverCache.clear()
+ hoverInfo.value = []
+ selectedTarget.value = null
+
+ // 3. 重新获取初始状态
+ await fetchInitialState()
+
+ // 4. 更新世界版本,触发特定组件重绘
+ worldVersion.value++
+
+ return true
+ }
+
function connect() {
gateway.connect()
}
@@ -195,12 +221,14 @@ export const useGameStore = defineStore('game', () => {
hoverInfo,
infoLoading,
infoError,
+ worldVersion, // 导出
connect,
disconnect,
fetchInitialState,
openInfoPanel,
closeInfoPanel,
setLongTermObjective,
- clearLongTermObjective
+ clearLongTermObjective,
+ reloadGame
}
})