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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 19:40:55 +08:00

676 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 游戏循环系统 - 主循环、时间推进、战斗处理、市场刷新
* Phase 7 核心实现
*/
import { GAME_CONSTANTS } from '@/config/constants.js'
import { ITEM_CONFIG } from '@/config/items.js'
import { SKILL_CONFIG } from '@/config/skills.js'
import { ENEMY_CONFIG } from '@/config/enemies.js'
import { combatTick, initCombat, getEnvironmentType } from './combatSystem.js'
import { processTaskTick } from './taskSystem.js'
import { processEnvironmentExp, processEnvironmentEffects } from './environmentSystem.js'
import { checkScheduledEvents } from './eventSystem.js'
import { addSkillExp } from './skillSystem.js'
import { addItemToInventory } from './itemSystem.js'
import { addExp, calculateCombatExp } from './levelingSystem.js'
// 游戏循环定时器
let gameLoopInterval = null
let isLoopRunning = false
// 离线时间记录
let lastSaveTime = Date.now()
/**
* 启动游戏循环
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {boolean} 是否成功启动
*/
export function startGameLoop(gameStore, playerStore) {
if (isLoopRunning) {
console.warn('Game loop is already running')
return false
}
// 每秒执行一次游戏tick
gameLoopInterval = setInterval(() => {
gameTick(gameStore, playerStore)
}, 1000)
isLoopRunning = true
console.log('Game loop started')
return true
}
/**
* 停止游戏循环
* @returns {boolean} 是否成功停止
*/
export function stopGameLoop() {
if (!isLoopRunning) {
return false
}
if (gameLoopInterval) {
clearInterval(gameLoopInterval)
gameLoopInterval = null
}
isLoopRunning = false
console.log('Game loop stopped')
return true
}
/**
* 检查游戏循环是否运行中
* @returns {boolean}
*/
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
* @param {Object} playerStore - 玩家Store
*/
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)
}
// 3. 处理任务
if (gameStore.activeTasks && gameStore.activeTasks.length > 0) {
const completedTasks = processTaskTick(gameStore, playerStore, 1000)
// 处理完成的任务奖励
for (const { task, rewards } of completedTasks) {
handleTaskCompletion(gameStore, playerStore, task, rewards)
}
}
// 4. 处理环境经验
processEnvironmentExp(
gameStore,
playerStore,
playerStore.currentLocation,
1
)
// 5. 处理环境负面效果
processEnvironmentEffects(
playerStore,
playerStore.currentLocation,
1
)
// 6. 检查定时事件
checkScheduledEvents(gameStore, playerStore)
// 7. 自动保存检查(每分钟保存一次)
const currentTime = Date.now()
if (currentTime - lastSaveTime >= 60000) {
// 触发自动保存由App.vue处理
lastSaveTime = currentTime
if (gameStore.triggerAutoSave) {
gameStore.triggerAutoSave()
}
}
}
/**
* 更新游戏时间
* 现实1秒 = 游戏时间5分钟
* @param {Object} gameStore - 游戏Store
*/
export function updateGameTime(gameStore) {
const { gameTime } = gameStore
// 每秒增加5分钟
gameTime.totalMinutes += GAME_CONSTANTS.TIME_SCALE
// 计算天、时、分
const minutesPerDay = 24 * 60
const minutesPerHour = 60
gameTime.day = Math.floor(gameTime.totalMinutes / minutesPerDay) + 1
const dayMinutes = gameTime.totalMinutes % minutesPerDay
gameTime.hour = Math.floor(dayMinutes / minutesPerHour)
gameTime.minute = dayMinutes % minutesPerHour
// 检查是否需要刷新市场价格(每天刷新一次)
if (gameTime.hour === 0 && gameTime.minute === 0) {
refreshMarketPrices(gameStore)
}
}
/**
* 处理战斗tick
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
*/
function handleCombatTick(gameStore, playerStore) {
const result = combatTick(gameStore, playerStore, gameStore.combatState)
// 添加战斗日志
if (result.logs && result.logs.length > 0) {
for (const log of result.logs) {
if (typeof log === 'string') {
gameStore.addLog(log, 'combat')
} else if (log.message) {
gameStore.addLog(log.message, log.type || 'combat')
}
}
}
// 处理战斗结果
if (result.victory) {
handleCombatVictory(gameStore, playerStore, result)
} else if (result.defeat) {
handleCombatDefeat(gameStore, playerStore, result)
} else if (result.enemy) {
// 更新敌人状态
gameStore.combatState.enemy = result.enemy
gameStore.combatState.ticks++
}
}
/**
* 处理战斗胜利
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} combatResult - 战斗结果
*/
export function handleCombatVictory(gameStore, playerStore, combatResult) {
const { enemy } = combatResult
// 结束战斗状态
gameStore.inCombat = false
gameStore.combatState = null
// 添加日志
gameStore.addLog(`战胜了 ${enemy.name}!`, 'reward')
// 收集所有经验获取信息,用于统一日志显示
const expGains = []
// 给予经验值 (使用新的等级系统)
if (enemy.expReward) {
const adjustedExp = calculateCombatExp(
enemy.level || 1,
playerStore.level.current,
enemy.expReward
)
const levelResult = addExp(playerStore, gameStore, adjustedExp)
expGains.push({
type: '战斗经验',
amount: adjustedExp,
leveledUp: levelResult.leveledUp
})
}
// 给予武器技能经验奖励
if (enemy.skillExpReward && playerStore.equipment.weapon) {
const weapon = playerStore.equipment.weapon
// 根据武器ID确定技能ID
let skillId = null
if (weapon.id === 'wooden_stick') {
skillId = 'stick_mastery'
} else if (weapon.subtype === 'sword') {
skillId = 'sword_mastery'
} else if (weapon.subtype === 'axe') {
skillId = 'axe_mastery'
}
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) {
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)
}
// 触发战斗胜利事件
gameStore.triggerEvent?.('combat', {
result: 'victory',
enemyId: enemy.id,
enemy
})
// 自动战斗模式:战斗结束后继续寻找敌人
if (gameStore.autoCombat) {
// 检查玩家状态是否允许继续战斗
const canContinue = playerStore.currentStats.health > 10 &&
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 {
// 状态过低,自动关闭自动战斗
gameStore.autoCombat = false
gameStore.addLog('状态过低,自动战斗已停止', 'warning')
}
}
}
/**
* 启动自动战斗(在当前位置寻找敌人)
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
*/
function startAutoCombat(gameStore, playerStore) {
// 检查当前位置是否有敌人
const locationId = playerStore.currentLocation
const locationEnemies = LOCATION_ENEMIES[locationId]
if (!locationEnemies || locationEnemies.length === 0) {
gameStore.autoCombat = false
gameStore.isSearching = false
gameStore.addLog('当前区域没有敌人,自动战斗已停止', 'info')
return
}
// 随机选择一个敌人
const enemyId = locationEnemies[Math.floor(Math.random() * locationEnemies.length)]
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)
gameStore.inCombat = true
}
// 当前区域的敌人配置
const LOCATION_ENEMIES = {
wild1: ['wild_dog'],
boss_lair: ['test_boss']
}
/**
* 处理战斗失败
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} combatResult - 战斗结果
*/
export function handleCombatDefeat(gameStore, playerStore, combatResult) {
// 结束战斗状态
gameStore.inCombat = false
gameStore.combatState = null
// 添加日志
gameStore.addLog('你被击败了...', 'error')
// 惩罚:回到营地
playerStore.currentLocation = 'camp'
// 恢复部分状态(在营地)
playerStore.currentStats.health = Math.floor(playerStore.currentStats.maxHealth * 0.3)
playerStore.currentStats.stamina = Math.floor(playerStore.currentStats.maxStamina * 0.5)
gameStore.addLog('你在营地醒来,损失了部分状态', 'info')
// 触发战斗失败事件
gameStore.triggerEvent?.('combat', {
result: 'defeat'
})
}
/**
* 处理掉落物品
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Array} drops - 掉落列表
*/
function processDrops(gameStore, playerStore, drops) {
for (const drop of drops) {
// 检查掉落概率
if (drop.chance && Math.random() > drop.chance) {
continue
}
// 随机数量
const count = drop.count
? drop.count.min + Math.floor(Math.random() * (drop.count.max - drop.count.min + 1))
: 1
// 添加物品
const result = addItemToInventory(playerStore, drop.itemId, count)
if (result.success) {
const item = result.item
gameStore.addLog(`获得了 ${item.name} x${count}`, 'reward')
}
}
}
/**
* 处理任务完成
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} task - 完成的任务
* @param {Object} rewards - 奖励
*/
function handleTaskCompletion(gameStore, playerStore, task, rewards) {
// 处理制造任务
if (task.type === 'crafting') {
if (rewards.success !== false && rewards.craftedItem) {
// 制造成功,添加物品到背包
const { completeCrafting } = require('./craftingSystem.js')
const result = completeCrafting(gameStore, playerStore, task, rewards)
if (result.success) {
gameStore.addLog(result.message, 'reward')
} else if (result.failed) {
gameStore.addLog(result.message, 'warning')
}
} else if (rewards.success === false) {
// 制造失败
gameStore.addLog('制造失败,材料已消耗', 'warning')
}
return
}
// 应用货币奖励
if (rewards.currency) {
playerStore.currency.copper += rewards.currency
}
// 应用技能经验
for (const [skillId, exp] of Object.entries(rewards)) {
if (skillId !== 'currency' && skillId !== 'completionBonus' && skillId !== 'craftedItem' && skillId !== 'craftedCount' && typeof exp === 'number') {
addSkillExp(playerStore, skillId, exp)
}
}
// 添加完成日志
const taskNames = {
reading: '阅读',
resting: '休息',
training: '训练',
working: '工作',
praying: '祈祷',
crafting: '制造'
}
gameStore.addLog(`${taskNames[task.type]}任务完成`, 'reward')
}
/**
* 刷新市场价格
* @param {Object} gameStore - 游戏Store
*/
export function refreshMarketPrices(gameStore) {
const prices = {}
// 为所有可交易物品生成新价格
for (const [itemId, config] of Object.entries(ITEM_CONFIG)) {
if (config.baseValue > 0 && config.type !== 'key') {
// 价格波动范围0.5 - 1.5倍
const fluctuation = 0.5 + Math.random()
prices[itemId] = {
buyRate: fluctuation * 2, // 购买价格倍率
sellRate: fluctuation * 0.3 // 出售价格倍率
}
}
}
gameStore.marketPrices = {
lastRefreshDay: gameStore.gameTime?.day || 1,
prices
}
gameStore.addLog('市场价格已更新', 'info')
}
/**
* 获取物品的当前市场价格
* @param {Object} gameStore - 游戏Store
* @param {String} itemId - 物品ID
* @returns {Object} { buyPrice: number, sellPrice: number }
*/
export function getMarketPrice(gameStore, itemId) {
const config = ITEM_CONFIG[itemId]
if (!config || !config.baseValue) {
return { buyPrice: 0, sellPrice: 0 }
}
const marketData = gameStore.marketPrices?.prices?.[itemId]
const basePrice = config.baseValue
if (marketData) {
return {
buyPrice: Math.floor(basePrice * marketData.buyRate),
sellPrice: Math.floor(basePrice * marketData.sellRate)
}
}
// 默认价格
return {
buyPrice: Math.floor(basePrice * 2),
sellPrice: Math.floor(basePrice * 0.3)
}
}
/**
* 计算离线收益
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Number} offlineSeconds - 离线秒数
* @returns {Object} 离线收益信息
*/
export function calculateOfflineEarnings(gameStore, playerStore, offlineSeconds) {
const maxOfflineSeconds = 8 * 60 * 60 // 最多计算8小时
const effectiveSeconds = Math.min(offlineSeconds, maxOfflineSeconds)
const earnings = {
time: effectiveSeconds,
timeDisplay: formatOfflineTime(effectiveSeconds),
skillExp: {},
currency: 0
}
// 简化计算:基于离线时间给予少量技能经验
// 假设玩家在离线期间进行了基础训练
const baseExpRate = 0.1 // 每秒0.1经验
const totalExp = Math.floor(baseExpRate * effectiveSeconds)
// 将经验分配给已解锁的技能
const unlockedSkills = Object.keys(playerStore.skills || {}).filter(
skillId => playerStore.skills[skillId]?.unlocked
)
if (unlockedSkills.length > 0) {
const expPerSkill = Math.floor(totalExp / unlockedSkills.length)
for (const skillId of unlockedSkills) {
earnings.skillExp[skillId] = expPerSkill
}
}
// 离线货币收益(极少)
earnings.currency = Math.floor(effectiveSeconds * 0.01)
return earnings
}
/**
* 应用离线收益
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} earnings - 离线收益
*/
export function applyOfflineEarnings(gameStore, playerStore, earnings) {
// 应用技能经验
for (const [skillId, exp] of Object.entries(earnings.skillExp)) {
addSkillExp(playerStore, skillId, exp)
}
// 应用货币
if (earnings.currency > 0) {
playerStore.currency.copper += earnings.currency
}
// 添加日志
gameStore.addLog(
`离线 ${earnings.timeDisplay},获得了一些经验`,
'info'
)
}
/**
* 格式化离线时间
* @param {Number} seconds - 秒数
* @returns {String} 格式化的时间
*/
function formatOfflineTime(seconds) {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
}
return `${minutes}分钟`
}
/**
* 更新最后保存时间
*/
export function updateLastSaveTime() {
lastSaveTime = Date.now()
}
/**
* 获取最后保存时间
* @returns {Number} 时间戳
*/
export function getLastSaveTime() {
return lastSaveTime
}