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 @@ + + + + + 选择出售数量 + + {{ quantitySelector.item.icon }} {{ quantitySelector.item.name }} + 拥有: {{ quantitySelector.maxCount }} + + + + {{ quantitySelector.selectedCount }} + + + + + + + 预计获得: + {{ getSellPrice(quantitySelector.item) * quantitySelector.selectedCount }} 铜币 + + + + + + + + @@ -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 @@ /> + + + 🔍 寻找中... + 正在寻找新的敌人 + + + + + + + 可前往的区域 @@ -380,6 +391,8 @@ function toggleAutoCombat() { } } } else { + // 关闭自动战斗时,清除寻找中状态 + game.isSearching = false 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 { color: $text-secondary; font-size: 24rpx; diff --git a/components/panels/StatusPanel.vue b/components/panels/StatusPanel.vue index dd2f995..46ef49f 100644 --- a/components/panels/StatusPanel.vue +++ b/components/panels/StatusPanel.vue @@ -139,6 +139,7 @@ height="8rpx" :showText="false" /> + {{ skill.exp }}/{{ skill.maxExp }} @@ -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 }