diff --git a/components/common/ProgressBar.vue b/components/common/ProgressBar.vue
index fb2f509..e134a65 100644
--- a/components/common/ProgressBar.vue
+++ b/components/common/ProgressBar.vue
@@ -2,7 +2,7 @@
- {{ value }}/{{ max }}
+ {{ displayValue }}/{{ displayMax }}
@@ -22,6 +22,10 @@ const percentage = computed(() => {
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(() => ({
width: `${percentage.value}%`,
backgroundColor: props.color
diff --git a/components/drawers/InventoryDrawer.vue b/components/drawers/InventoryDrawer.vue
index 09ad88e..479ee7f 100644
--- a/components/drawers/InventoryDrawer.vue
+++ b/components/drawers/InventoryDrawer.vue
@@ -55,6 +55,12 @@
type="warning"
@click="unequipItem"
/>
+
{
return selectedItem.value.type === 'consumable'
})
+const canRead = computed(() => {
+ if (!selectedItem.value) return false
+ return selectedItem.value.type === 'book'
+})
+
// 装备槽位映射
const slotMap = {
weapon: 'weapon',
@@ -163,11 +175,34 @@ function useItem() {
if (!selectedItem.value) return
const result = useItemUtil(player, game, selectedItem.value.id, 1)
if (result.success) {
- // 如果阅读任务,可以在这里处理
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() {
if (!selectedItem.value) return
// 已装备的物品不能出售
@@ -256,13 +291,6 @@ function sellItem() {
font-size: 22rpx;
margin-top: 4rpx;
}
-}
-
-.quality-badge {
- font-size: 18rpx;
- opacity: 0.8;
- margin-left: 8rpx;
-}
&__equipped-badge {
width: 32rpx;
@@ -278,6 +306,12 @@ function sellItem() {
}
}
+.quality-badge {
+ font-size: 18rpx;
+ opacity: 0.8;
+ margin-left: 8rpx;
+}
+
.inventory-empty {
padding: 80rpx;
text-align: center;
diff --git a/components/drawers/ShopDrawer.vue b/components/drawers/ShopDrawer.vue
index b21cd51..4d4a748 100644
--- a/components/drawers/ShopDrawer.vue
+++ b/components/drawers/ShopDrawer.vue
@@ -130,6 +130,34 @@
+
+
+
@@ -138,6 +166,7 @@ import { ref, computed } from 'vue'
import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
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 FilterTabs from '@/components/common/FilterTabs.vue'
import TextButton from '@/components/common/TextButton.vue'
@@ -149,6 +178,8 @@ const currentMode = ref('buy')
const selectedItem = ref(null)
const bulkMode = ref(false)
const selectedItems = ref([])
+// 数量选择弹窗状态
+const quantitySelector = ref(null) // { item: Object, maxCount: Number, selectedCount: Number }
const emit = defineEmits(['close'])
@@ -171,10 +202,18 @@ const shopItems = computed(() => {
return config.items
.filter(item => item.stock === -1 || item.stock > 0)
.map(shopItem => {
- const itemConfig = { ...shopItem }
- itemConfig.id = shopItem.itemId
- return itemConfig
+ // 从 ITEM_CONFIG 获取完整物品信息
+ const itemConfig = ITEM_CONFIG[shopItem.itemId]
+ 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) {
if (bulkMode.value) {
- // 批量模式:切换选中状态
+ // 批量模式:切换选中状态(批量出售出售全部)
const index = selectedItems.value.findIndex(i =>
(i.uniqueId && i.uniqueId === item.uniqueId) || i.id === item.id
)
@@ -258,8 +297,18 @@ function handleSellItemClick(item) {
selectedItems.value.push(item)
}
} else {
- // 普通模式:显示详情
- trySell(item)
+ // 普通模式:检查是否是堆叠物品
+ if (item.count > 1) {
+ // 堆叠物品:显示数量选择弹窗
+ quantitySelector.value = {
+ item: item,
+ maxCount: item.count,
+ selectedCount: 1
+ }
+ } else {
+ // 非堆叠物品:直接显示详情
+ trySell(item)
+ }
}
}
@@ -338,20 +387,68 @@ function confirmSell() {
if (!selectedItem.value) return
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) {
// 增加金币
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
}
+// 确认数量选择出售
+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() {
emit('close')
}
@@ -630,4 +727,58 @@ function close() {
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;
+ }
+}
diff --git a/components/panels/MapPanel.vue b/components/panels/MapPanel.vue
index 3cd9134..7416ebe 100644
--- a/components/panels/MapPanel.vue
+++ b/components/panels/MapPanel.vue
@@ -32,6 +32,17 @@
/>
+
+
+ 🔍 寻找中...
+ 正在寻找新的敌人
+
+ ●
+ ●
+ ●
+
+
+
@@ -261,7 +262,7 @@ import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
import { SKILL_CONFIG } from '@/config/skills'
import { ITEM_CONFIG } from '@/config/items'
-import { startTask } from '@/utils/taskSystem'
+import { startTask, endTask } from '@/utils/taskSystem'
import { getSkillDisplayInfo } from '@/utils/skillSystem'
import ProgressBar from '@/components/common/ProgressBar.vue'
import StatItem from '@/components/common/StatItem.vue'
@@ -426,7 +427,7 @@ const trainingTask = computed(() => {
function startTraining(skill) {
const result = startTask(game, player, 'training', {
skillId: skill.id,
- duration: 300 // 5分钟
+ duration: 60 // 1分钟
})
if (result.success) {
@@ -438,7 +439,6 @@ function startTraining(skill) {
function stopTraining() {
if (trainingTask.value) {
- const { endTask } = require('@/utils/taskSystem')
endTask(game, player, trainingTask.value.taskId, false)
game.addLog('停止了训练', 'info')
}
@@ -631,6 +631,15 @@ function cancelActiveTask(task) {
&__bar {
flex: 1;
min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+ }
+
+ &__progress {
+ color: $text-muted;
+ font-size: 20rpx;
+ text-align: right;
}
}
diff --git a/store/game.js b/store/game.js
index 7d1bffc..c5409e3 100644
--- a/store/game.js
+++ b/store/game.js
@@ -28,6 +28,7 @@ export const useGameStore = defineStore('game', () => {
const inCombat = ref(false)
const combatState = ref(null)
const autoCombat = ref(false) // 自动战斗模式
+ const isSearching = ref(false) // 正在寻找敌人中(自动战斗间隔期间)
// 活动任务
const activeTasks = ref([])
@@ -77,6 +78,7 @@ export const useGameStore = defineStore('game', () => {
inCombat.value = false
combatState.value = null
autoCombat.value = false
+ isSearching.value = false
activeTasks.value = []
negativeStatus.value = []
marketPrices.value = {
@@ -94,6 +96,7 @@ export const useGameStore = defineStore('game', () => {
inCombat,
combatState,
autoCombat,
+ isSearching,
activeTasks,
negativeStatus,
marketPrices,
diff --git a/utils/gameLoop.js b/utils/gameLoop.js
index 08a491a..808132a 100644
--- a/utils/gameLoop.js
+++ b/utils/gameLoop.js
@@ -71,6 +71,81 @@ export function isGameLoopRunning() {
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 - 每秒执行一次
* @param {Object} gameStore - 游戏Store
@@ -80,6 +155,11 @@ export function gameTick(gameStore, playerStore) {
// 1. 更新游戏时间
updateGameTime(gameStore)
+ // 1.5. 自然回复(仅在非战斗、非训练状态)
+ if (!gameStore.inCombat && !isPlayerTraining(gameStore)) {
+ processNaturalRegeneration(gameStore, playerStore)
+ }
+
// 2. 处理战斗
if (gameStore.inCombat && gameStore.combatState) {
handleCombatTick(gameStore, playerStore)
@@ -197,6 +277,9 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
// 添加日志
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
+ // 收集所有经验获取信息,用于统一日志显示
+ const expGains = []
+
// 给予经验值 (使用新的等级系统)
if (enemy.expReward) {
const adjustedExp = calculateCombatExp(
@@ -204,7 +287,12 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
playerStore.level.current,
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) {
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) {
- const skillName = SKILL_CONFIG[skillId]?.name || skillId
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) {
processDrops(gameStore, playerStore, enemy.drops)
@@ -249,11 +355,17 @@ export function handleCombatVictory(gameStore, playerStore, combatResult) {
playerStore.currentStats.stamina > 10
if (canContinue) {
+ // 设置寻找中状态
+ gameStore.isSearching = true
+ gameStore.addLog('正在寻找新的敌人...', 'info')
+
// 延迟3秒后开始下一场战斗
setTimeout(() => {
if (!gameStore.inCombat && gameStore.autoCombat) {
startAutoCombat(gameStore, playerStore)
}
+ // 清除寻找中状态(无论是否成功找到敌人)
+ gameStore.isSearching = false
}, 3000)
} else {
// 状态过低,自动关闭自动战斗
@@ -275,6 +387,7 @@ function startAutoCombat(gameStore, playerStore) {
if (!locationEnemies || locationEnemies.length === 0) {
gameStore.autoCombat = false
+ gameStore.isSearching = false
gameStore.addLog('当前区域没有敌人,自动战斗已停止', 'info')
return
}
@@ -284,10 +397,13 @@ function startAutoCombat(gameStore, playerStore) {
const enemyConfig = ENEMY_CONFIG[enemyId]
if (!enemyConfig) {
+ gameStore.isSearching = false
gameStore.addLog('敌人配置错误', 'error')
return
}
+ // 清除寻找中状态
+ gameStore.isSearching = false
gameStore.addLog(`自动寻找中...遇到了 ${enemyConfig.name}!`, 'combat')
const environment = getEnvironmentType(locationId)
gameStore.combatState = initCombat(enemyId, enemyConfig, environment)
diff --git a/utils/storage.js b/utils/storage.js
index b86efe0..7840534 100644
--- a/utils/storage.js
+++ b/utils/storage.js
@@ -155,6 +155,7 @@ function serializeGameData(gameStore) {
activeTasks: JSON.parse(JSON.stringify(gameStore.activeTasks || [])),
negativeStatus: JSON.parse(JSON.stringify(gameStore.negativeStatus || [])),
marketPrices: JSON.parse(JSON.stringify(gameStore.marketPrices || {})),
+ autoCombat: gameStore.autoCombat || false, // 保存自动战斗状态
// 不保存日志(logs不持久化)
// 不保存战斗状态(重新登录时退出战斗)
// 不保存当前事件(重新登录时清除)
@@ -262,6 +263,11 @@ function applyGameData(gameStore, data) {
gameStore.marketPrices = JSON.parse(JSON.stringify(data.marketPrices))
}
+ // 自动战斗状态
+ if (data.autoCombat !== undefined) {
+ gameStore.autoCombat = data.autoCombat
+ }
+
// 定时事件
if (data.scheduledEvents) {
gameStore.scheduledEvents = JSON.parse(JSON.stringify(data.scheduledEvents))
diff --git a/utils/taskSystem.js b/utils/taskSystem.js
index 021b8d3..dc683d1 100644
--- a/utils/taskSystem.js
+++ b/utils/taskSystem.js
@@ -312,6 +312,9 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
const readingTime = bookConfig.readingTime || 60
+ // 首次阅读时解锁阅读技能
+ unlockSkill(playerStore, 'reading')
+
// 检查环境惩罚
const location = playerStore.currentLocation
let timeMultiplier = 1.0
@@ -322,9 +325,26 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
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 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) {
// 阅读完成
const rewards = {}
@@ -343,13 +363,13 @@ function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
// 添加日志
if (gameStore.addLog) {
- gameStore.addLog(`读完了《${bookConfig.name}》`, 'reward')
+ gameStore.addLog(`读完了《${bookConfig.name}》,阅读经验+${Math.floor(readingExpGain * task.progress / readingTime)}`, 'reward')
}
return { completed: true, rewards }
}
- // 阅读进行中,给予少量经验
+ // 阅读进行中,给予其他技能经验
if (bookConfig.expReward) {
for (const [skillId, expPerSecond] of Object.entries(bookConfig.expReward)) {
const expPerTick = (expPerSecond / readingTime) * elapsedSeconds * timeMultiplier
@@ -412,8 +432,8 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
return { completed: true, error: '技能不存在' }
}
- // 消耗耐力
- const staminaCost = 2 * elapsedSeconds
+ // 消耗耐力(降低消耗)
+ const staminaCost = 1 * elapsedSeconds
if (playerStore.currentStats.stamina < staminaCost) {
// 耐力不足,任务结束
return { completed: true, rewards: {} }
@@ -421,8 +441,8 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
playerStore.currentStats.stamina -= staminaCost
- // 给予技能经验
- const expPerSecond = 1
+ // 给予技能经验(提升获取速度)
+ const expPerSecond = 5 // 从1提升到5
const expGain = expPerSecond * elapsedSeconds
if (!task.accumulatedExp) {
@@ -433,6 +453,12 @@ function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
}
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) {
const rewards = { [skillId]: task.accumulatedExp[skillId] || 0 }