feat: 扩展游戏内容和地图系统优化

- MiniMap: 添加缩放/平移功能,优化节点显示样式
- 新增洞穴相关敌人和Boss(洞穴蝙蝠、洞穴领主)
- 新增义体类物品(钢制义臂、光学义眼、真皮护甲)
- 扩展武器技能系统(剑、斧、钝器、弓箭精通)
- 更新商店配置和义体相关功能
- 完善玩家/游戏Store状态管理

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-02-02 15:54:49 +08:00
parent ccfd6a5e75
commit 7b851656de
8 changed files with 511 additions and 111 deletions

View File

@@ -2,40 +2,70 @@
<view class="mini-map">
<!-- 地图容器 -->
<view class="mini-map__container">
<!-- 连接线层 -->
<view class="mini-map__connections">
<view
v-for="edge in mapData.edges"
:key="`${edge.from}-${edge.to}`"
class="connection-line"
:class="{ 'connection-line--path': isInPath(edge) }"
:style="getLineStyle(edge)"
></view>
</view>
<!-- 节点层 -->
<view
v-for="node in mapData.nodes"
:key="node.id"
class="map-node"
:class="getNodeClass(node)"
:style="getNodeStyle(node)"
@click="handleNodeClick(node)"
<scroll-view
class="mini-map__scroll"
scroll-x
scroll-y
:scroll-left="scrollLeft"
:scroll-top="scrollTop"
@scroll="onScroll"
>
<!-- 节点圆点 -->
<view class="node__dot"></view>
<view
class="mini-map__content"
:style="contentStyle"
>
<!-- 连接线层 -->
<view class="mini-map__connections">
<view
v-for="edge in mapData.edges"
:key="`${edge.from}-${edge.to}`"
class="connection-line"
:class="{ 'connection-line--path': isInPath(edge) }"
:style="getLineStyle(edge)"
></view>
</view>
<!-- 当前位置脉冲效果 -->
<view v-if="node.id === currentLocation" class="node__pulse"></view>
<!-- 节点层 -->
<view
v-for="node in mapData.nodes"
:key="node.id"
class="map-node"
:class="getNodeClass(node)"
:style="getNodeStyle(node)"
@click="handleNodeClick(node)"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<!-- 节点圆点 -->
<view class="node__dot"></view>
<!-- 节点名称 -->
<text class="node__name">{{ node.name }}</text>
<!-- 当前位置脉冲效果 -->
<view v-if="node.id === currentLocation" class="node__pulse"></view>
<!-- 锁定图标 -->
<text v-if="isLocked(node)" class="node__lock">🔒</text>
<!-- 节点名称 -->
<text class="node__name">{{ node.name }}</text>
<!-- 距离标记 -->
<text v-else-if="getDistance(node) > 0" class="node__distance">{{ getDistance(node) }}</text>
<!-- 锁定图标 -->
<text v-if="isLocked(node)" class="node__lock">🔒</text>
<!-- 距离标记 -->
<text v-else-if="getDistance(node) > 0" class="node__distance">{{ getDistance(node) }}</text>
</view>
</view>
</scroll-view>
<!-- 缩放控制 -->
<view class="mini-map__zoom-controls">
<view class="zoom-btn" @click="zoomIn">
<text class="zoom-icon">+</text>
</view>
<view class="zoom-btn" @click="resetZoom">
<text class="zoom-icon"></text>
</view>
<view class="zoom-btn" @click="zoomOut">
<text class="zoom-icon"></text>
</view>
</view>
</view>
@@ -79,28 +109,45 @@ const player = usePlayerStore()
const mapData = ref({ nodes: [], edges: [], width: 300, height: 400 })
const selectedPath = ref([])
// 缩放和平移状态
const zoom = ref(1)
const scrollLeft = ref(0)
const scrollTop = ref(0)
const currentLocationNode = ref(null)
// 容器尺寸
const containerWidth = 280
const containerHeight = 300
// 触摸拖动状态
const touchStartX = ref(0)
const touchStartY = ref(0)
const isDragging = ref(false)
const currentLocation = computed(() => player.currentLocation)
// 内容样式
const contentStyle = computed(() => {
return {
width: (containerWidth * zoom.value) + 'px',
height: (containerHeight * zoom.value) + 'px',
transformOrigin: 'center center'
}
})
// 初始化地图
function initMap() {
const data = getMapData(player.currentLocation)
// 缩放坐标以适应容器
const containerWidth = 280
const containerHeight = 340
const scaleX = containerWidth / data.width
const scaleY = containerHeight / data.height
const scale = Math.min(scaleX, scaleY, 1)
// 居中偏移
const offsetX = (containerWidth - data.width * scale) / 2 + 10
const offsetY = (containerHeight - data.height * scale) / 2 + 10
// 不再缩放以适应容器,使用原始坐标
// 但确保地图足够大
const padding = 40
mapData.value = {
nodes: data.nodes.map(n => ({
...n,
x: n.x * scale + offsetX,
y: n.y * scale + offsetY
x: n.x,
y: n.y
})),
edges: data.edges,
width: data.width,
@@ -108,21 +155,74 @@ function initMap() {
reachable: data.reachable
}
// 更新缩放比例供样式使用
updateNodeStyles()
// 找到当前节点并居中
centerOnLocation()
selectedPath.value = []
}
// 更新节点样式转换为rpx
function updateNodeStyles() {
// 在实际渲染时使用百分比或rpx
// 居中到当前位置
function centerOnLocation() {
const currentNode = mapData.value.nodes.find(n => n.id === currentLocation.value)
if (!currentNode) return
currentLocationNode.value = currentNode
// 计算需要滚动的位置以居中当前节点
// 考虑缩放比例
const targetLeft = currentNode.x * zoom.value - containerWidth / 2
const targetTop = currentNode.y * zoom.value - containerHeight / 2
scrollLeft.value = Math.max(0, targetLeft)
scrollTop.value = Math.max(0, targetTop)
}
// 缩放控制
function zoomIn() {
zoom.value = Math.min(2, zoom.value + 0.2)
centerOnLocation()
}
function zoomOut() {
zoom.value = Math.max(0.5, zoom.value - 0.2)
centerOnLocation()
}
function resetZoom() {
zoom.value = 1
centerOnLocation()
}
// 触摸事件处理(用于拖动)
function onTouchStart(e) {
touchStartX.value = e.touches[0].clientX
touchStartY.value = e.touches[0].clientY
isDragging.value = false
}
function onTouchMove(e) {
const deltaX = e.touches[0].clientX - touchStartX.value
const deltaY = e.touches[0].clientY - touchStartY.value
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
isDragging.value = true
}
}
function onTouchEnd(e) {
// 如果不是拖动,则视为点击
// 这里的处理在 handleNodeClick 中完成
}
// 滚动事件
function onScroll(e) {
// 可以在这里记录滚动位置
}
// 获取节点样式
function getNodeStyle(node) {
return {
left: node.x + 'px',
top: node.y + 'px'
left: (node.x * zoom.value) + 'px',
top: (node.y * zoom.value) + 'px'
}
}
@@ -180,14 +280,14 @@ function getLineStyle(edge) {
if (!fromNode || !toNode) return {}
const dx = toNode.x - fromNode.x
const dy = toNode.y - fromNode.y
const dx = (toNode.x - fromNode.x) * zoom.value
const dy = (toNode.y - fromNode.y) * zoom.value
const length = Math.sqrt(dx * dx + dy * dy)
const angle = Math.atan2(dy, dx) * 180 / Math.PI
return {
left: fromNode.x + 'px',
top: fromNode.y + 'px',
left: (fromNode.x * zoom.value) + 'px',
top: (fromNode.y * zoom.value) + 'px',
width: length + 'px',
transform: `rotate(${angle}deg)`
}
@@ -252,6 +352,17 @@ onMounted(() => {
border: 2rpx solid #4a5568;
}
&__scroll {
width: 100%;
height: 300rpx;
}
&__content {
position: relative;
min-width: 100%;
min-height: 100%;
}
&__connections {
position: absolute;
top: 0;
@@ -276,6 +387,38 @@ onMounted(() => {
border: 1rpx solid $accent;
border-radius: 8rpx;
}
&__zoom-controls {
position: absolute;
bottom: 12rpx;
right: 12rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
z-index: 20;
}
}
.zoom-btn {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba($bg-primary, 0.8);
border: 1rpx solid rgba($accent, 0.5);
border-radius: 8rpx;
backdrop-filter: blur(4rpx);
&:active {
background-color: rgba($accent, 0.3);
}
}
.zoom-icon {
font-size: 32rpx;
color: $accent;
font-weight: bold;
}
.connection-line {