feat: 优化游戏体验和系统平衡性

- 修复商店物品名称显示问题,添加堆叠物品出售数量选择
- 自动战斗状态持久化,战斗结束显示"寻找中"状态
- 战斗日志显示经验获取详情(战斗经验、武器经验)
- 技能进度条显示当前/最大经验值
- 阅读自动解锁技能并持续获得阅读经验,背包可直接阅读
- 优化训练平衡:时长60秒,经验5点/秒,耐力消耗降低
- 实现自然回复系统:基于体质回复HP/耐力,休息提供3倍加成
- 战斗和训练时不进行自然回复

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-23 19:40:55 +08:00
parent 16223c89a5
commit cef974d94f
9 changed files with 447 additions and 29 deletions

View File

@@ -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)