Initial commit: Text Adventure Game
Features: - Combat system with AP/EP hit calculation and three-layer defense - Auto-combat/farming mode - Item system with stacking support - Skill system with levels, milestones, and parent skill sync - Shop system with dynamic pricing - Inventory management with bulk selling - Event system - Game loop with offline earnings - Save/Load system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
549
utils/gameLoop.js
Normal file
549
utils/gameLoop.js
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* 游戏循环系统 - 主循环、时间推进、战斗处理、市场刷新
|
||||
* 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'
|
||||
|
||||
// 游戏循环定时器
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏主tick - 每秒执行一次
|
||||
* @param {Object} gameStore - 游戏Store
|
||||
* @param {Object} playerStore - 玩家Store
|
||||
*/
|
||||
export function gameTick(gameStore, playerStore) {
|
||||
// 1. 更新游戏时间
|
||||
updateGameTime(gameStore)
|
||||
|
||||
// 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')
|
||||
|
||||
// 给予经验值
|
||||
if (enemy.expReward) {
|
||||
playerStore.level.exp += enemy.expReward
|
||||
|
||||
// 检查升级
|
||||
const maxExp = playerStore.level.maxExp
|
||||
if (playerStore.level.exp >= maxExp) {
|
||||
playerStore.level.current++
|
||||
playerStore.level.exp -= maxExp
|
||||
playerStore.level.maxExp = Math.floor(playerStore.level.maxExp * 1.5)
|
||||
|
||||
gameStore.addLog(`升级了! 等级: ${playerStore.level.current}`, 'reward')
|
||||
|
||||
// 升级恢复状态
|
||||
playerStore.currentStats.health = playerStore.currentStats.maxHealth
|
||||
playerStore.currentStats.stamina = playerStore.currentStats.maxStamina
|
||||
playerStore.currentStats.sanity = playerStore.currentStats.maxSanity
|
||||
}
|
||||
}
|
||||
|
||||
// 给予武器技能经验奖励
|
||||
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)
|
||||
|
||||
if (result.leveledUp) {
|
||||
const skillName = SKILL_CONFIG[skillId]?.name || skillId
|
||||
gameStore.addLog(`${skillName}升级到了 Lv.${result.newLevel}!`, 'reward')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理掉落
|
||||
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) {
|
||||
// 延迟3秒后开始下一场战斗
|
||||
setTimeout(() => {
|
||||
if (!gameStore.inCombat && gameStore.autoCombat) {
|
||||
startAutoCombat(gameStore, playerStore)
|
||||
}
|
||||
}, 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.addLog('当前区域没有敌人,自动战斗已停止', 'info')
|
||||
return
|
||||
}
|
||||
|
||||
// 随机选择一个敌人
|
||||
const enemyId = locationEnemies[Math.floor(Math.random() * locationEnemies.length)]
|
||||
const enemyConfig = ENEMY_CONFIG[enemyId]
|
||||
|
||||
if (!enemyConfig) {
|
||||
gameStore.addLog('敌人配置错误', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
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 (rewards.currency) {
|
||||
playerStore.currency.copper += rewards.currency
|
||||
}
|
||||
|
||||
// 应用技能经验
|
||||
for (const [skillId, exp] of Object.entries(rewards)) {
|
||||
if (skillId !== 'currency' && skillId !== 'completionBonus' && typeof exp === 'number') {
|
||||
addSkillExp(playerStore, skillId, exp)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加完成日志
|
||||
const taskNames = {
|
||||
reading: '阅读',
|
||||
resting: '休息',
|
||||
training: '训练',
|
||||
working: '工作',
|
||||
praying: '祈祷'
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user