feat: 优化游戏体验和系统平衡性
- 修复商店物品名称显示问题,添加堆叠物品出售数量选择 - 自动战斗状态持久化,战斗结束显示"寻找中"状态 - 战斗日志显示经验获取详情(战斗经验、武器经验) - 技能进度条显示当前/最大经验值 - 阅读自动解锁技能并持续获得阅读经验,背包可直接阅读 - 优化训练平衡:时长60秒,经验5点/秒,耐力消耗降低 - 实现自然回复系统:基于体质回复HP/耐力,休息提供3倍加成 - 战斗和训练时不进行自然回复 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<view class="progress-bar" :style="{ height }">
|
<view class="progress-bar" :style="{ height }">
|
||||||
<view class="progress-bar__fill" :style="fillStyle"></view>
|
<view class="progress-bar__fill" :style="fillStyle"></view>
|
||||||
<view v-if="showText" class="progress-bar__text">
|
<view v-if="showText" class="progress-bar__text">
|
||||||
{{ value }}/{{ max }}
|
{{ displayValue }}/{{ displayMax }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,6 +22,10 @@ const percentage = computed(() => {
|
|||||||
return Math.min(100, Math.max(0, (props.value / props.max) * 100))
|
return Math.min(100, Math.max(0, (props.value / props.max) * 100))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const displayValue = computed(() => Math.floor(props.value))
|
||||||
|
|
||||||
|
const displayMax = computed(() => Math.floor(props.max))
|
||||||
|
|
||||||
const fillStyle = computed(() => ({
|
const fillStyle = computed(() => ({
|
||||||
width: `${percentage.value}%`,
|
width: `${percentage.value}%`,
|
||||||
backgroundColor: props.color
|
backgroundColor: props.color
|
||||||
|
|||||||
@@ -55,6 +55,12 @@
|
|||||||
type="warning"
|
type="warning"
|
||||||
@click="unequipItem"
|
@click="unequipItem"
|
||||||
/>
|
/>
|
||||||
|
<TextButton
|
||||||
|
v-if="canRead"
|
||||||
|
text="阅读"
|
||||||
|
type="primary"
|
||||||
|
@click="readItem"
|
||||||
|
/>
|
||||||
<TextButton
|
<TextButton
|
||||||
v-if="canUse"
|
v-if="canUse"
|
||||||
text="使用"
|
text="使用"
|
||||||
@@ -75,6 +81,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { useGameStore } from '@/store/game'
|
import { useGameStore } from '@/store/game'
|
||||||
import { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
|
import { equipItem as equipItemUtil, unequipItemBySlot, useItem as useItemUtil } from '@/utils/itemSystem.js'
|
||||||
|
import { startTask } from '@/utils/taskSystem.js'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
|
|
||||||
@@ -112,6 +119,11 @@ const canUse = computed(() => {
|
|||||||
return selectedItem.value.type === 'consumable'
|
return selectedItem.value.type === 'consumable'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canRead = computed(() => {
|
||||||
|
if (!selectedItem.value) return false
|
||||||
|
return selectedItem.value.type === 'book'
|
||||||
|
})
|
||||||
|
|
||||||
// 装备槽位映射
|
// 装备槽位映射
|
||||||
const slotMap = {
|
const slotMap = {
|
||||||
weapon: 'weapon',
|
weapon: 'weapon',
|
||||||
@@ -163,11 +175,34 @@ function useItem() {
|
|||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
const result = useItemUtil(player, game, selectedItem.value.id, 1)
|
const result = useItemUtil(player, game, selectedItem.value.id, 1)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 如果阅读任务,可以在这里处理
|
|
||||||
selectedItem.value = null
|
selectedItem.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readItem() {
|
||||||
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
|
// 检查是否已经在阅读中
|
||||||
|
const existingTask = game.activeTasks?.find(t => t.type === 'reading')
|
||||||
|
if (existingTask) {
|
||||||
|
game.addLog('已经在阅读其他书籍了', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = startTask(game, player, 'reading', {
|
||||||
|
itemId: selectedItem.value.id,
|
||||||
|
duration: selectedItem.value.readingTime || 60
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
game.addLog(`开始阅读《${selectedItem.value.name}》`, 'info')
|
||||||
|
selectedItem.value = null
|
||||||
|
emit('close') // 关闭背包以便看到阅读进度
|
||||||
|
} else {
|
||||||
|
game.addLog(result.message, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sellItem() {
|
function sellItem() {
|
||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
// 已装备的物品不能出售
|
// 已装备的物品不能出售
|
||||||
@@ -256,13 +291,6 @@ function sellItem() {
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
margin-top: 4rpx;
|
margin-top: 4rpx;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.quality-badge {
|
|
||||||
font-size: 18rpx;
|
|
||||||
opacity: 0.8;
|
|
||||||
margin-left: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__equipped-badge {
|
&__equipped-badge {
|
||||||
width: 32rpx;
|
width: 32rpx;
|
||||||
@@ -278,6 +306,12 @@ function sellItem() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality-badge {
|
||||||
|
font-size: 18rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.inventory-empty {
|
.inventory-empty {
|
||||||
padding: 80rpx;
|
padding: 80rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -130,6 +130,34 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 数量选择弹窗 -->
|
||||||
|
<view v-if="quantitySelector" class="item-detail-popup" @click="cancelQuantitySell">
|
||||||
|
<view class="item-detail" @click.stop>
|
||||||
|
<text class="item-detail__name">选择出售数量</text>
|
||||||
|
<view class="quantity-selector">
|
||||||
|
<text class="quantity-selector__item-name">{{ quantitySelector.item.icon }} {{ quantitySelector.item.name }}</text>
|
||||||
|
<text class="quantity-selector__available">拥有: {{ quantitySelector.maxCount }}</text>
|
||||||
|
|
||||||
|
<view class="quantity-selector__controls">
|
||||||
|
<TextButton text="-" type="default" @click="decreaseSellCount" :disabled="quantitySelector.selectedCount <= 1" />
|
||||||
|
<text class="quantity-selector__count">{{ quantitySelector.selectedCount }}</text>
|
||||||
|
<TextButton text="+" type="default" @click="increaseSellCount" :disabled="quantitySelector.selectedCount >= quantitySelector.maxCount" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<TextButton text="全部" type="default" size="small" @click="setMaxSellCount" style="margin-top: 12rpx;" />
|
||||||
|
|
||||||
|
<view class="quantity-selector__preview">
|
||||||
|
<text class="quantity-selector__price-label">预计获得:</text>
|
||||||
|
<text class="quantity-selector__price">{{ getSellPrice(quantitySelector.item) * quantitySelector.selectedCount }} 铜币</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-detail__actions">
|
||||||
|
<TextButton text="确认出售" type="primary" @click="confirmQuantitySell" />
|
||||||
|
<TextButton text="取消" @click="cancelQuantitySell" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -138,6 +166,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { usePlayerStore } from '@/store/player'
|
import { usePlayerStore } from '@/store/player'
|
||||||
import { useGameStore } from '@/store/game'
|
import { useGameStore } from '@/store/game'
|
||||||
import { getShopConfig, getBuyPrice as calcBuyPrice, getSellPrice as calcSellPrice } from '@/config/shop.js'
|
import { getShopConfig, getBuyPrice as calcBuyPrice, getSellPrice as calcSellPrice } from '@/config/shop.js'
|
||||||
|
import { ITEM_CONFIG } from '@/config/items.js'
|
||||||
import { addItemToInventory, removeItemFromInventory } from '@/utils/itemSystem.js'
|
import { addItemToInventory, removeItemFromInventory } from '@/utils/itemSystem.js'
|
||||||
import FilterTabs from '@/components/common/FilterTabs.vue'
|
import FilterTabs from '@/components/common/FilterTabs.vue'
|
||||||
import TextButton from '@/components/common/TextButton.vue'
|
import TextButton from '@/components/common/TextButton.vue'
|
||||||
@@ -149,6 +178,8 @@ const currentMode = ref('buy')
|
|||||||
const selectedItem = ref(null)
|
const selectedItem = ref(null)
|
||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const selectedItems = ref([])
|
const selectedItems = ref([])
|
||||||
|
// 数量选择弹窗状态
|
||||||
|
const quantitySelector = ref(null) // { item: Object, maxCount: Number, selectedCount: Number }
|
||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
@@ -171,10 +202,18 @@ const shopItems = computed(() => {
|
|||||||
return config.items
|
return config.items
|
||||||
.filter(item => item.stock === -1 || item.stock > 0)
|
.filter(item => item.stock === -1 || item.stock > 0)
|
||||||
.map(shopItem => {
|
.map(shopItem => {
|
||||||
const itemConfig = { ...shopItem }
|
// 从 ITEM_CONFIG 获取完整物品信息
|
||||||
itemConfig.id = shopItem.itemId
|
const itemConfig = ITEM_CONFIG[shopItem.itemId]
|
||||||
return itemConfig
|
if (!itemConfig) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
...itemConfig, // 复制所有物品属性(name, icon, description等)
|
||||||
|
id: shopItem.itemId, // 使用 itemId 作为 id
|
||||||
|
stock: shopItem.stock,
|
||||||
|
baseStock: shopItem.baseStock
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 玩家可出售物品
|
// 玩家可出售物品
|
||||||
@@ -248,7 +287,7 @@ function isItemSelected(item) {
|
|||||||
// 处理出售列表点击
|
// 处理出售列表点击
|
||||||
function handleSellItemClick(item) {
|
function handleSellItemClick(item) {
|
||||||
if (bulkMode.value) {
|
if (bulkMode.value) {
|
||||||
// 批量模式:切换选中状态
|
// 批量模式:切换选中状态(批量出售出售全部)
|
||||||
const index = selectedItems.value.findIndex(i =>
|
const index = selectedItems.value.findIndex(i =>
|
||||||
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
|
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
|
||||||
)
|
)
|
||||||
@@ -258,9 +297,19 @@ function handleSellItemClick(item) {
|
|||||||
selectedItems.value.push(item)
|
selectedItems.value.push(item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 普通模式:显示详情
|
// 普通模式:检查是否是堆叠物品
|
||||||
|
if (item.count > 1) {
|
||||||
|
// 堆叠物品:显示数量选择弹窗
|
||||||
|
quantitySelector.value = {
|
||||||
|
item: item,
|
||||||
|
maxCount: item.count,
|
||||||
|
selectedCount: 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非堆叠物品:直接显示详情
|
||||||
trySell(item)
|
trySell(item)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全选
|
// 全选
|
||||||
@@ -338,20 +387,68 @@ function confirmSell() {
|
|||||||
if (!selectedItem.value) return
|
if (!selectedItem.value) return
|
||||||
|
|
||||||
const item = selectedItem.value
|
const item = selectedItem.value
|
||||||
const price = getSellPrice(item)
|
const sellCount = item.sellCount || 1
|
||||||
|
const price = getSellPrice(item) * sellCount
|
||||||
|
|
||||||
// 移除物品
|
// 移除物品
|
||||||
const result = removeItemFromInventory(player, item.uniqueId || item.id)
|
const result = removeItemFromInventory(player, item.uniqueId || item.id, sellCount)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 增加金币
|
// 增加金币
|
||||||
player.currency.copper += price
|
player.currency.copper += price
|
||||||
game.addLog(`出售了 ${item.name},获得 ${price} 铜币`, 'reward')
|
const countText = sellCount > 1 ? `x${sellCount}` : ''
|
||||||
|
game.addLog(`出售了 ${item.name}${countText},获得 ${price} 铜币`, 'reward')
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedItem.value = null
|
selectedItem.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确认数量选择出售
|
||||||
|
function confirmQuantitySell() {
|
||||||
|
if (!quantitySelector.value) return
|
||||||
|
|
||||||
|
const { item, selectedCount } = quantitySelector.value
|
||||||
|
const price = getSellPrice(item) * selectedCount
|
||||||
|
|
||||||
|
// 移除指定数量的物品
|
||||||
|
const result = removeItemFromInventory(player, item.uniqueId || item.id, selectedCount)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 增加金币
|
||||||
|
player.currency.copper += price
|
||||||
|
game.addLog(`出售了 ${item.name}x${selectedCount},获得 ${price} 铜币`, 'reward')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
quantitySelector.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消数量选择
|
||||||
|
function cancelQuantitySell() {
|
||||||
|
quantitySelector.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加出售数量
|
||||||
|
function increaseSellCount() {
|
||||||
|
if (quantitySelector.value && quantitySelector.value.selectedCount < quantitySelector.value.maxCount) {
|
||||||
|
quantitySelector.value.selectedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减少出售数量
|
||||||
|
function decreaseSellCount() {
|
||||||
|
if (quantitySelector.value && quantitySelector.value.selectedCount > 1) {
|
||||||
|
quantitySelector.value.selectedCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置最大出售数量
|
||||||
|
function setMaxSellCount() {
|
||||||
|
if (quantitySelector.value) {
|
||||||
|
quantitySelector.value.selectedCount = quantitySelector.value.maxCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
@@ -630,4 +727,58 @@ function close() {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数量选择器样式
|
||||||
|
.quantity-selector {
|
||||||
|
&__item-name {
|
||||||
|
display: block;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__available {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__count {
|
||||||
|
min-width: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-primary;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: $bg-tertiary;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price-label {
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__price {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,6 +32,17 @@
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 寻找敌人中状态 -->
|
||||||
|
<view v-if="game.isSearching && !game.inCombat" class="searching-status">
|
||||||
|
<text class="searching-status__title">🔍 寻找中...</text>
|
||||||
|
<text class="searching-status__desc">正在寻找新的敌人</text>
|
||||||
|
<view class="searching-status__dots">
|
||||||
|
<text class="dot">●</text>
|
||||||
|
<text class="dot">●</text>
|
||||||
|
<text class="dot">●</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 区域列表 -->
|
<!-- 区域列表 -->
|
||||||
<view class="map-panel__locations-header">
|
<view class="map-panel__locations-header">
|
||||||
<text class="locations-title">可前往的区域</text>
|
<text class="locations-title">可前往的区域</text>
|
||||||
@@ -380,6 +391,8 @@ function toggleAutoCombat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 关闭自动战斗时,清除寻找中状态
|
||||||
|
game.isSearching = false
|
||||||
game.addLog('自动战斗已关闭', 'info')
|
game.addLog('自动战斗已关闭', 'info')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +485,62 @@ function toggleAutoCombat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 寻找中状态样式
|
||||||
|
.searching-status {
|
||||||
|
padding: 16rpx;
|
||||||
|
background-color: rgba($accent, 0.1);
|
||||||
|
border: 1rpx solid $accent;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: block;
|
||||||
|
color: $accent;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
display: block;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
color: $accent;
|
||||||
|
font-size: 24rpx;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.locations-title {
|
.locations-title {
|
||||||
color: $text-secondary;
|
color: $text-secondary;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@
|
|||||||
height="8rpx"
|
height="8rpx"
|
||||||
:showText="false"
|
:showText="false"
|
||||||
/>
|
/>
|
||||||
|
<text class="skill-item__progress">{{ skill.exp }}/{{ skill.maxExp }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
||||||
@@ -261,7 +262,7 @@ import { usePlayerStore } from '@/store/player'
|
|||||||
import { useGameStore } from '@/store/game'
|
import { useGameStore } from '@/store/game'
|
||||||
import { SKILL_CONFIG } from '@/config/skills'
|
import { SKILL_CONFIG } from '@/config/skills'
|
||||||
import { ITEM_CONFIG } from '@/config/items'
|
import { ITEM_CONFIG } from '@/config/items'
|
||||||
import { startTask } from '@/utils/taskSystem'
|
import { startTask, endTask } from '@/utils/taskSystem'
|
||||||
import { getSkillDisplayInfo } from '@/utils/skillSystem'
|
import { getSkillDisplayInfo } from '@/utils/skillSystem'
|
||||||
import ProgressBar from '@/components/common/ProgressBar.vue'
|
import ProgressBar from '@/components/common/ProgressBar.vue'
|
||||||
import StatItem from '@/components/common/StatItem.vue'
|
import StatItem from '@/components/common/StatItem.vue'
|
||||||
@@ -426,7 +427,7 @@ const trainingTask = computed(() => {
|
|||||||
function startTraining(skill) {
|
function startTraining(skill) {
|
||||||
const result = startTask(game, player, 'training', {
|
const result = startTask(game, player, 'training', {
|
||||||
skillId: skill.id,
|
skillId: skill.id,
|
||||||
duration: 300 // 5分钟
|
duration: 60 // 1分钟
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -438,7 +439,6 @@ function startTraining(skill) {
|
|||||||
|
|
||||||
function stopTraining() {
|
function stopTraining() {
|
||||||
if (trainingTask.value) {
|
if (trainingTask.value) {
|
||||||
const { endTask } = require('@/utils/taskSystem')
|
|
||||||
endTask(game, player, trainingTask.value.taskId, false)
|
endTask(game, player, trainingTask.value.taskId, false)
|
||||||
game.addLog('停止了训练', 'info')
|
game.addLog('停止了训练', 'info')
|
||||||
}
|
}
|
||||||
@@ -631,6 +631,15 @@ function cancelActiveTask(task) {
|
|||||||
&__bar {
|
&__bar {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 20rpx;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
const inCombat = ref(false)
|
const inCombat = ref(false)
|
||||||
const combatState = ref(null)
|
const combatState = ref(null)
|
||||||
const autoCombat = ref(false) // 自动战斗模式
|
const autoCombat = ref(false) // 自动战斗模式
|
||||||
|
const isSearching = ref(false) // 正在寻找敌人中(自动战斗间隔期间)
|
||||||
|
|
||||||
// 活动任务
|
// 活动任务
|
||||||
const activeTasks = ref([])
|
const activeTasks = ref([])
|
||||||
@@ -77,6 +78,7 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
inCombat.value = false
|
inCombat.value = false
|
||||||
combatState.value = null
|
combatState.value = null
|
||||||
autoCombat.value = false
|
autoCombat.value = false
|
||||||
|
isSearching.value = false
|
||||||
activeTasks.value = []
|
activeTasks.value = []
|
||||||
negativeStatus.value = []
|
negativeStatus.value = []
|
||||||
marketPrices.value = {
|
marketPrices.value = {
|
||||||
@@ -94,6 +96,7 @@ export const useGameStore = defineStore('game', () => {
|
|||||||
inCombat,
|
inCombat,
|
||||||
combatState,
|
combatState,
|
||||||
autoCombat,
|
autoCombat,
|
||||||
|
isSearching,
|
||||||
activeTasks,
|
activeTasks,
|
||||||
negativeStatus,
|
negativeStatus,
|
||||||
marketPrices,
|
marketPrices,
|
||||||
|
|||||||
@@ -71,6 +71,81 @@ export function isGameLoopRunning() {
|
|||||||
return isLoopRunning
|
return isLoopRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否正在休息
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isPlayerResting(gameStore) {
|
||||||
|
return gameStore.activeTasks?.some(task => task.type === 'resting') || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查玩家是否正在训练
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isPlayerTraining(gameStore) {
|
||||||
|
return gameStore.activeTasks?.some(task => task.type === 'training') || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理自然回复
|
||||||
|
* 基于体质属性自然回复HP和耐力,休息时提供额外加成
|
||||||
|
* 注意:战斗中和训练时不进行自然回复
|
||||||
|
* @param {Object} gameStore - 游戏Store
|
||||||
|
* @param {Object} playerStore - 玩家Store
|
||||||
|
*/
|
||||||
|
function processNaturalRegeneration(gameStore, playerStore) {
|
||||||
|
const { currentStats, baseStats } = playerStore
|
||||||
|
const isResting = isPlayerResting(gameStore)
|
||||||
|
|
||||||
|
// 体质影响HP回复量:每点体质每秒回复0.05点HP
|
||||||
|
// 体质10 = 每秒0.5点HP基础回复
|
||||||
|
const vitality = baseStats?.vitality || 10
|
||||||
|
const baseHpRegen = vitality * 0.05
|
||||||
|
|
||||||
|
// 耐力基础回复:每秒回复1点
|
||||||
|
const baseStaminaRegen = 1
|
||||||
|
|
||||||
|
// 休息时回复倍率
|
||||||
|
const restMultiplier = isResting ? 3 : 1
|
||||||
|
|
||||||
|
// 计算实际回复量
|
||||||
|
const hpRegen = baseHpRegen * restMultiplier
|
||||||
|
const staminaRegen = baseStaminaRegen * restMultiplier
|
||||||
|
|
||||||
|
// 记录旧值用于日志
|
||||||
|
const oldHp = currentStats.health
|
||||||
|
const oldStamina = currentStats.stamina
|
||||||
|
|
||||||
|
// 应用回复(不超过最大值)
|
||||||
|
const newHp = Math.min(currentStats.maxHealth, currentStats.health + hpRegen)
|
||||||
|
const newStamina = Math.min(currentStats.maxStamina, currentStats.stamina + staminaRegen)
|
||||||
|
|
||||||
|
currentStats.health = newHp
|
||||||
|
currentStats.stamina = newStamina
|
||||||
|
|
||||||
|
// 只在有明显回复且每30秒输出一次日志
|
||||||
|
if (gameStore.lastRegenLogTime === undefined) {
|
||||||
|
gameStore.lastRegenLogTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
const shouldLog = (now - gameStore.lastRegenLogTime) > 30000 && // 30秒
|
||||||
|
((newHp > oldHp && newHp < currentStats.maxHealth) ||
|
||||||
|
(newStamina > oldStamina && newStamina < currentStats.maxStamina))
|
||||||
|
|
||||||
|
if (shouldLog) {
|
||||||
|
gameStore.lastRegenLogTime = now
|
||||||
|
if (isResting) {
|
||||||
|
// 休息中的日志由休息任务处理,这里不重复输出
|
||||||
|
} else if (newHp > oldHp && newStamina > oldStamina) {
|
||||||
|
gameStore.addLog('自然回复:HP和耐力缓慢恢复中...', 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 游戏主tick - 每秒执行一次
|
* 游戏主tick - 每秒执行一次
|
||||||
* @param {Object} gameStore - 游戏Store
|
* @param {Object} gameStore - 游戏Store
|
||||||
@@ -80,6 +155,11 @@ export function gameTick(gameStore, playerStore) {
|
|||||||
// 1. 更新游戏时间
|
// 1. 更新游戏时间
|
||||||
updateGameTime(gameStore)
|
updateGameTime(gameStore)
|
||||||
|
|
||||||
|
// 1.5. 自然回复(仅在非战斗、非训练状态)
|
||||||
|
if (!gameStore.inCombat && !isPlayerTraining(gameStore)) {
|
||||||
|
processNaturalRegeneration(gameStore, playerStore)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 处理战斗
|
// 2. 处理战斗
|
||||||
if (gameStore.inCombat && gameStore.combatState) {
|
if (gameStore.inCombat && gameStore.combatState) {
|
||||||
handleCombatTick(gameStore, playerStore)
|
handleCombatTick(gameStore, playerStore)
|
||||||
@@ -197,6 +277,9 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
// 添加日志
|
// 添加日志
|
||||||
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
|
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
|
||||||
|
|
||||||
|
// 收集所有经验获取信息,用于统一日志显示
|
||||||
|
const expGains = []
|
||||||
|
|
||||||
// 给予经验值 (使用新的等级系统)
|
// 给予经验值 (使用新的等级系统)
|
||||||
if (enemy.expReward) {
|
if (enemy.expReward) {
|
||||||
const adjustedExp = calculateCombatExp(
|
const adjustedExp = calculateCombatExp(
|
||||||
@@ -204,7 +287,12 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
playerStore.level.current,
|
playerStore.level.current,
|
||||||
enemy.expReward
|
enemy.expReward
|
||||||
)
|
)
|
||||||
addExp(playerStore, gameStore, adjustedExp)
|
const levelResult = addExp(playerStore, gameStore, adjustedExp)
|
||||||
|
expGains.push({
|
||||||
|
type: '战斗经验',
|
||||||
|
amount: adjustedExp,
|
||||||
|
leveledUp: levelResult.leveledUp
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给予武器技能经验奖励
|
// 给予武器技能经验奖励
|
||||||
@@ -222,14 +310,32 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
|
|
||||||
if (skillId) {
|
if (skillId) {
|
||||||
const result = addSkillExp(playerStore, skillId, enemy.skillExpReward)
|
const result = addSkillExp(playerStore, skillId, enemy.skillExpReward)
|
||||||
|
const skillName = SKILL_CONFIG[skillId]?.name || skillId
|
||||||
|
expGains.push({
|
||||||
|
type: `${skillName}经验`,
|
||||||
|
amount: enemy.skillExpReward,
|
||||||
|
leveledUp: result.leveledUp,
|
||||||
|
newLevel: result.newLevel
|
||||||
|
})
|
||||||
|
|
||||||
if (result.leveledUp) {
|
if (result.leveledUp) {
|
||||||
const skillName = SKILL_CONFIG[skillId]?.name || skillId
|
|
||||||
gameStore.addLog(`${skillName}升级到了 Lv.${result.newLevel}!`, 'reward')
|
gameStore.addLog(`${skillName}升级到了 Lv.${result.newLevel}!`, 'reward')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一显示经验获取日志
|
||||||
|
if (expGains.length > 0) {
|
||||||
|
const expText = expGains.map(e => {
|
||||||
|
let text = `${e.type}+${e.amount}`
|
||||||
|
if (e.leveledUp) {
|
||||||
|
text += ` ⬆`
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}).join(', ')
|
||||||
|
gameStore.addLog(`获得: ${expText}`, 'info')
|
||||||
|
}
|
||||||
|
|
||||||
// 处理掉落
|
// 处理掉落
|
||||||
if (enemy.drops && enemy.drops.length > 0) {
|
if (enemy.drops && enemy.drops.length > 0) {
|
||||||
processDrops(gameStore, playerStore, enemy.drops)
|
processDrops(gameStore, playerStore, enemy.drops)
|
||||||
@@ -249,11 +355,17 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
|
|||||||
playerStore.currentStats.stamina > 10
|
playerStore.currentStats.stamina > 10
|
||||||
|
|
||||||
if (canContinue) {
|
if (canContinue) {
|
||||||
|
// 设置寻找中状态
|
||||||
|
gameStore.isSearching = true
|
||||||
|
gameStore.addLog('正在寻找新的敌人...', 'info')
|
||||||
|
|
||||||
// 延迟3秒后开始下一场战斗
|
// 延迟3秒后开始下一场战斗
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!gameStore.inCombat && gameStore.autoCombat) {
|
if (!gameStore.inCombat && gameStore.autoCombat) {
|
||||||
startAutoCombat(gameStore, playerStore)
|
startAutoCombat(gameStore, playerStore)
|
||||||
}
|
}
|
||||||
|
// 清除寻找中状态(无论是否成功找到敌人)
|
||||||
|
gameStore.isSearching = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
} else {
|
} else {
|
||||||
// 状态过低,自动关闭自动战斗
|
// 状态过低,自动关闭自动战斗
|
||||||
@@ -275,6 +387,7 @@ function startAutoCombat(gameStore, playerStore) {
|
|||||||
|
|
||||||
if (!locationEnemies || locationEnemies.length === 0) {
|
if (!locationEnemies || locationEnemies.length === 0) {
|
||||||
gameStore.autoCombat = false
|
gameStore.autoCombat = false
|
||||||
|
gameStore.isSearching = false
|
||||||
gameStore.addLog('当前区域没有敌人,自动战斗已停止', 'info')
|
gameStore.addLog('当前区域没有敌人,自动战斗已停止', 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -284,10 +397,13 @@ function startAutoCombat(gameStore, playerStore) {
|
|||||||
const enemyConfig = ENEMY_CONFIG[enemyId]
|
const enemyConfig = ENEMY_CONFIG[enemyId]
|
||||||
|
|
||||||
if (!enemyConfig) {
|
if (!enemyConfig) {
|
||||||
|
gameStore.isSearching = false
|
||||||
gameStore.addLog('敌人配置错误', 'error')
|
gameStore.addLog('敌人配置错误', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清除寻找中状态
|
||||||
|
gameStore.isSearching = false
|
||||||
gameStore.addLog(`自动寻找中...遇到了 ${enemyConfig.name}!`, 'combat')
|
gameStore.addLog(`自动寻找中...遇到了 ${enemyConfig.name}!`, 'combat')
|
||||||
const environment = getEnvironmentType(locationId)
|
const environment = getEnvironmentType(locationId)
|
||||||
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
|
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ function serializeGameData(gameStore) {
|
|||||||
activeTasks: JSON.parse(JSON.stringify(gameStore.activeTasks || [])),
|
activeTasks: JSON.parse(JSON.stringify(gameStore.activeTasks || [])),
|
||||||
negativeStatus: JSON.parse(JSON.stringify(gameStore.negativeStatus || [])),
|
negativeStatus: JSON.parse(JSON.stringify(gameStore.negativeStatus || [])),
|
||||||
marketPrices: JSON.parse(JSON.stringify(gameStore.marketPrices || {})),
|
marketPrices: JSON.parse(JSON.stringify(gameStore.marketPrices || {})),
|
||||||
|
autoCombat: gameStore.autoCombat || false, // 保存自动战斗状态
|
||||||
// 不保存日志(logs不持久化)
|
// 不保存日志(logs不持久化)
|
||||||
// 不保存战斗状态(重新登录时退出战斗)
|
// 不保存战斗状态(重新登录时退出战斗)
|
||||||
// 不保存当前事件(重新登录时清除)
|
// 不保存当前事件(重新登录时清除)
|
||||||
@@ -262,6 +263,11 @@ function applyGameData(gameStore, data) {
|
|||||||
gameStore.marketPrices = JSON.parse(JSON.stringify(data.marketPrices))
|
gameStore.marketPrices = JSON.parse(JSON.stringify(data.marketPrices))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动战斗状态
|
||||||
|
if (data.autoCombat !== undefined) {
|
||||||
|
gameStore.autoCombat = data.autoCombat
|
||||||
|
}
|
||||||
|
|
||||||
// 定时事件
|
// 定时事件
|
||||||
if (data.scheduledEvents) {
|
if (data.scheduledEvents) {
|
||||||
gameStore.scheduledEvents = JSON.parse(JSON.stringify(data.scheduledEvents))
|
gameStore.scheduledEvents = JSON.parse(JSON.stringify(data.scheduledEvents))
|
||||||
|
|||||||
@@ -312,6 +312,9 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
|
|
||||||
const readingTime = bookConfig.readingTime || 60
|
const readingTime = bookConfig.readingTime || 60
|
||||||
|
|
||||||
|
// 首次阅读时解锁阅读技能
|
||||||
|
unlockSkill(playerStore, 'reading')
|
||||||
|
|
||||||
// 检查环境惩罚
|
// 检查环境惩罚
|
||||||
const location = playerStore.currentLocation
|
const location = playerStore.currentLocation
|
||||||
let timeMultiplier = 1.0
|
let timeMultiplier = 1.0
|
||||||
@@ -322,9 +325,26 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
timeMultiplier = 0.5 + (darkPenaltyReduce / 100) * 0.5
|
timeMultiplier = 0.5 + (darkPenaltyReduce / 100) * 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 阅读技能速度加成
|
||||||
|
const readingSkillLevel = playerStore.skills.reading?.level || 0
|
||||||
|
const readingSpeedBonus = playerStore.globalBonus?.readingSpeed || 1
|
||||||
|
timeMultiplier *= readingSpeedBonus
|
||||||
|
|
||||||
// 计算有效进度
|
// 计算有效进度
|
||||||
const effectiveProgress = task.progress * timeMultiplier
|
const effectiveProgress = task.progress * timeMultiplier
|
||||||
|
|
||||||
|
// 给予阅读技能经验(每秒给予经验)
|
||||||
|
const readingExpPerSecond = 1
|
||||||
|
const readingExpGain = readingExpPerSecond * elapsedSeconds * timeMultiplier
|
||||||
|
const readingResult = addSkillExp(playerStore, 'reading', readingExpGain)
|
||||||
|
|
||||||
|
// 检查技能是否升级
|
||||||
|
if (readingResult.leveledUp) {
|
||||||
|
if (gameStore.addLog) {
|
||||||
|
gameStore.addLog(`阅读技能升级到了 Lv.${readingResult.newLevel}!`, 'reward')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (effectiveProgress >= readingTime) {
|
if (effectiveProgress >= readingTime) {
|
||||||
// 阅读完成
|
// 阅读完成
|
||||||
const rewards = {}
|
const rewards = {}
|
||||||
@@ -343,13 +363,13 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
|
|
||||||
// 添加日志
|
// 添加日志
|
||||||
if (gameStore.addLog) {
|
if (gameStore.addLog) {
|
||||||
gameStore.addLog(`读完了《${bookConfig.name}》`, 'reward')
|
gameStore.addLog(`读完了《${bookConfig.name}》,阅读经验+${Math.floor(readingExpGain * task.progress / readingTime)}`, 'reward')
|
||||||
}
|
}
|
||||||
|
|
||||||
return { completed: true, rewards }
|
return { completed: true, rewards }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阅读进行中,给予少量经验
|
// 阅读进行中,给予其他技能经验
|
||||||
if (bookConfig.expReward) {
|
if (bookConfig.expReward) {
|
||||||
for (const [skillId, expPerSecond] of Object.entries(bookConfig.expReward)) {
|
for (const [skillId, expPerSecond] of Object.entries(bookConfig.expReward)) {
|
||||||
const expPerTick = (expPerSecond / readingTime) * elapsedSeconds * timeMultiplier
|
const expPerTick = (expPerSecond / readingTime) * elapsedSeconds * timeMultiplier
|
||||||
@@ -412,8 +432,8 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
return { completed: true, error: '技能不存在' }
|
return { completed: true, error: '技能不存在' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消耗耐力
|
// 消耗耐力(降低消耗)
|
||||||
const staminaCost = 2 * elapsedSeconds
|
const staminaCost = 1 * elapsedSeconds
|
||||||
if (playerStore.currentStats.stamina < staminaCost) {
|
if (playerStore.currentStats.stamina < staminaCost) {
|
||||||
// 耐力不足,任务结束
|
// 耐力不足,任务结束
|
||||||
return { completed: true, rewards: {} }
|
return { completed: true, rewards: {} }
|
||||||
@@ -421,8 +441,8 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
|
|
||||||
playerStore.currentStats.stamina -= staminaCost
|
playerStore.currentStats.stamina -= staminaCost
|
||||||
|
|
||||||
// 给予技能经验
|
// 给予技能经验(提升获取速度)
|
||||||
const expPerSecond = 1
|
const expPerSecond = 5 // 从1提升到5
|
||||||
const expGain = expPerSecond * elapsedSeconds
|
const expGain = expPerSecond * elapsedSeconds
|
||||||
|
|
||||||
if (!task.accumulatedExp) {
|
if (!task.accumulatedExp) {
|
||||||
@@ -433,6 +453,12 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
|
|||||||
}
|
}
|
||||||
task.accumulatedExp[skillId] += expGain
|
task.accumulatedExp[skillId] += expGain
|
||||||
|
|
||||||
|
// 检查技能是否升级
|
||||||
|
const result = addSkillExp(playerStore, skillId, expGain)
|
||||||
|
if (result.leveledUp && gameStore.addLog) {
|
||||||
|
gameStore.addLog(`${skillConfig.name}升级到了 Lv.${result.newLevel}!`, 'reward')
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否完成
|
// 检查是否完成
|
||||||
if (task.data.duration && task.progress >= task.data.duration) {
|
if (task.data.duration && task.progress >= task.data.duration) {
|
||||||
const rewards = { [skillId]: task.accumulatedExp[skillId] || 0 }
|
const rewards = { [skillId]: task.accumulatedExp[skillId] || 0 }
|
||||||
|
|||||||
Reference in New Issue
Block a user