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:
Claude
2026-01-21 17:13:51 +08:00
commit cb412544e9
90 changed files with 17149 additions and 0 deletions

583
utils/combatSystem.js Normal file
View File

@@ -0,0 +1,583 @@
/**
* 战斗系统 - AP/EP命中计算、三层防御、战斗tick处理
* Phase 6 核心系统实现
*/
import { LOCATION_CONFIG } from '@/config/locations.js'
/**
* 计算AP攻击点数
* AP = 灵巧 + 智力*0.2 + 技能加成 + 装备加成 + 姿态加成 - 敌人数惩罚
*
* @param {Object} attacker - 攻击者对象(玩家或敌人)
* @param {String} stance - 战斗姿态 'attack' | 'defense' | 'balance' | 'rapid' | 'heavy'
* @param {Number} enemyCount - 敌人数量(用于计算多敌人惩罚)
* @param {String} environment - 环境类型 'normal' | 'dark' | 'narrow' | 'harsh'
* @param {Object} bonuses - 额外加成 { skillBonus: number, equipBonus: number, darkPenaltyReduce: number }
* @returns {Number} AP值
*/
export function calculateAP(attacker, stance = 'balance', enemyCount = 1, environment = 'normal', bonuses = {}) {
const {
skillBonus = 0,
equipBonus = 0,
darkPenaltyReduce = 0
} = bonuses
// 基础AP = 灵巧 + 智力 * 0.2
let ap = (attacker.baseStats?.dexterity || 0) + (attacker.baseStats?.intuition || 0) * 0.2
// 技能加成
ap += skillBonus
// 装备加成
ap += equipBonus
// 姿态加成
const stanceModifiers = {
attack: 1.1, // 攻击姿态: AP +10%
defense: 0.9, // 防御姿态: AP -10%
balance: 1.0, // 平衡姿态: 无加成
rapid: 1.05, // 快速打击: AP +5%
heavy: 1.15 // 重击: AP +15%
}
ap *= stanceModifiers[stance] || 1.0
// 环境惩罚
if (environment === 'dark') {
// 黑暗环境 AP -20%,可被夜视技能减少
const darkPenalty = 0.2 * (1 - darkPenaltyReduce / 100)
ap *= (1 - darkPenalty)
} else if (environment === 'narrow') {
// 狭窄空间对AP无直接影响
ap *= 1.0
} else if (environment === 'harsh') {
// 恶劣环境 AP -10%
ap *= 0.9
}
// 多敌人惩罚每多1个敌人AP -5%,最多-30%
if (enemyCount > 1) {
const penalty = Math.min(0.3, (enemyCount - 1) * 0.05)
ap *= (1 - penalty)
}
// 耐力惩罚
if (attacker.currentStats && attacker.currentStats.stamina !== undefined) {
const staminaRatio = attacker.currentStats.stamina / (attacker.currentStats.maxStamina || 100)
if (staminaRatio === 0) {
ap *= 0.5 // 耐力耗尽AP减半
} else if (staminaRatio < 0.5) {
// 耐力低于50%时逐渐降低
ap *= (0.5 + staminaRatio * 0.5)
}
}
return Math.max(1, Math.floor(ap))
}
/**
* 计算EP闪避点数
* EP = 敏捷 + 智力*0.2 + 技能加成 + 装备加成 + 姿态加成 - 环境惩罚
*
* @param {Object} defender - 防御者对象(玩家或敌人)
* @param {String} stance - 战斗姿态 'attack' | 'defense' | 'balance' | 'rapid' | 'heavy'
* @param {String} environment - 环境类型
* @param {Object} bonuses - 额外加成
* @returns {Number} EP值
*/
export function calculateEP(defender, stance = 'balance', environment = 'normal', bonuses = {}) {
const {
skillBonus = 0,
equipBonus = 0,
darkPenaltyReduce = 0
} = bonuses
// 基础EP = 敏捷 + 智力 * 0.2
let ep = (defender.baseStats?.agility || 0) + (defender.baseStats?.intuition || 0) * 0.2
// 技能加成
ep += skillBonus
// 装备加成
ep += equipBonus
// 姿态加成
const stanceModifiers = {
attack: 0.8, // 攻击姿态: EP -20%
defense: 1.2, // 防御姿态: EP +20%
balance: 1.0, // 平衡姿态: 无加成
rapid: 0.7, // 快速打击: EP -30%
heavy: 0.85 // 重击: EP -15%
}
ep *= stanceModifiers[stance] || 1.0
// 环境惩罚
if (environment === 'dark') {
// 黑暗环境 EP -20%,可被夜视技能减少
const darkPenalty = 0.2 * (1 - darkPenaltyReduce / 100)
ep *= (1 - darkPenalty)
} else if (environment === 'narrow') {
// 狭窄空间 EP -30%
ep *= 0.7
} else if (environment === 'harsh') {
// 恶劣环境 EP -10%
ep *= 0.9
}
// 耐力惩罚
if (defender.currentStats && defender.currentStats.stamina !== undefined) {
const staminaRatio = defender.currentStats.stamina / (defender.currentStats.maxStamina || 100)
if (staminaRatio === 0) {
ep *= 0.5
} else if (staminaRatio < 0.5) {
ep *= (0.5 + staminaRatio * 0.5)
}
}
return Math.max(1, Math.floor(ep))
}
/**
* 计算命中概率(非线性公式)
* 基于Yet Another Idle RPG的设计
*
* @param {Number} ap - 攻击点数
* @param {Number} ep - 闪避点数
* @returns {Number} 命中概率 (0-1)
*/
export function calculateHitRate(ap, ep) {
const ratio = ap / (ep + 1)
// 非线性命中概率表
if (ratio >= 5) return 0.98
if (ratio >= 3) return 0.90
if (ratio >= 2) return 0.80
if (ratio >= 1.5) return 0.65
if (ratio >= 1) return 0.50
if (ratio >= 0.75) return 0.38
if (ratio >= 0.5) return 0.24
if (ratio >= 0.33) return 0.15
if (ratio >= 0.2) return 0.08
return 0.05
}
/**
* 计算暴击概率
* @param {Object} attacker - 攻击者
* @param {Object} bonuses - 额外加成
* @returns {Number} 暴击概率 (0-1)
*/
export function calculateCritRate(attacker, bonuses = {}) {
const { critRateBonus = 0 } = bonuses
// 基础暴击率 = 灵巧 / 100 + 全局加成
const baseCritRate = (attacker.baseStats?.dexterity || 0) / 100
const globalBonus = attacker.globalBonus?.critRate || 0
return Math.min(0.8, baseCritRate + (globalBonus + critRateBonus) / 100)
}
/**
* 计算暴击伤害倍数
* @param {Object} attacker - 攻击者
* @returns {Number} 暴击倍数
*/
export function calculateCritMultiplier(attacker) {
const baseMult = 1.5
const bonusMult = attacker.globalBonus?.critMult || 0
return baseMult + bonusMult
}
/**
* 处理单次攻击(三层防御机制)
* 1. 闪避判定
* 2. 护盾吸收
* 3. 防御力减少
*
* @param {Object} attacker - 攻击者对象
* @param {Object} defender - 防御者对象
* @param {String} stance - 攻击者姿态
* @param {String} environment - 环境类型
* @param {Object} attackerBonuses - 攻击者加成
* @param {Object} defenderBonuses - 防御者加成
* @returns {Object} { hit: boolean, damage: number, crit: boolean, evaded: boolean, shieldAbsorbed: number }
*/
export function processAttack(attacker, defender, stance = 'balance', environment = 'normal', attackerBonuses = {}, defenderBonuses = {}) {
// 计算AP和EP
const ap = calculateAP(attacker, stance, 1, environment, attackerBonuses)
const ep = calculateEP(defender, 'balance', environment, defenderBonuses)
// 计算命中概率
const hitRate = calculateHitRate(ap, ep)
// 第一步:闪避判定
const roll = Math.random()
if (roll > hitRate) {
return {
hit: false,
damage: 0,
crit: false,
evaded: true,
shieldAbsorbed: 0,
blocked: false
}
}
// 命中成功,计算伤害
let damage = attacker.baseStats?.attack || attacker.attack || 0
// 武器伤害加成(如果装备了武器)
if (attacker.weaponDamage) {
damage += attacker.weaponDamage
}
// 姿态对伤害的影响
const stanceDamageMod = {
attack: 1.5, // 攻击姿态: 伤害 +50%
defense: 0.7, // 防御姿态: 伤害 -30%
balance: 1.0,
rapid: 0.6, // 快速打击: 伤害 -40%
heavy: 2.0 // 重击: 伤害 +100%
}
damage *= stanceDamageMod[stance] || 1.0
// 暴击判定
const critRate = calculateCritRate(attacker, attackerBonuses)
const crit = Math.random() < critRate
if (crit) {
const critMult = calculateCritMultiplier(attacker)
damage *= critMult
}
// 第二步:护盾吸收
let shieldAbsorbed = 0
const shield = defender.shield || defender.currentStats?.shield || 0
if (shield > 0) {
const blockRate = defender.blockRate || 0 // 格挡率
const shouldBlock = Math.random() < blockRate
if (shouldBlock) {
// 护盾完全吸收伤害(可能还有剩余)
shieldAbsorbed = Math.min(shield, damage)
damage -= shieldAbsorbed
// 更新护盾值
if (defender.currentStats) {
defender.currentStats.shield = Math.max(0, shield - shieldAbsorbed)
}
if (damage <= 0) {
return {
hit: true,
damage: 0,
crit,
evaded: false,
shieldAbsorbed,
blocked: true
}
}
}
}
// 第三步:防御力减少
const defense = defender.baseStats?.defense || defender.defense || 0
// 计算减免后伤害
let reducedDamage = damage - defense
// 最小伤害为原始伤害的10%防御力最多减少90%
const minDamage = Math.max(1, damage * 0.1)
reducedDamage = Math.max(minDamage, reducedDamage)
return {
hit: true,
damage: Math.max(1, Math.floor(reducedDamage)),
crit,
evaded: false,
shieldAbsorbed,
blocked: false
}
}
/**
* 构建玩家战斗者对象(包含武器伤害)
* @param {Object} playerStore - 玩家Store
* @returns {Object} 战斗者对象
*/
function buildPlayerAttacker(playerStore) {
// 基础攻击力 = 力量
const baseAttack = playerStore.baseStats?.strength || 10
// 武器伤害
let weaponDamage = 0
const weapon = playerStore.equipment?.weapon
if (weapon && weapon.baseDamage) {
weaponDamage = weapon.baseDamage
}
// 技能加成(木棍精通等)
const skillId = weapon?.type === 'weapon' ? getWeaponSkillId(weapon) : null
let skillBonus = 0
if (skillId && playerStore.skills?.[skillId]?.level > 0) {
// 每级增加5%武器伤害
skillBonus = Math.floor(weaponDamage * playerStore.skills[skillId].level * 0.05)
}
// 全局加成
const attackBonus = playerStore.globalBonus?.attackBonus || 0
return {
...playerStore,
baseStats: {
...playerStore.baseStats,
attack: baseAttack + attackBonus,
dexterity: playerStore.baseStats?.dexterity || 8,
intuition: playerStore.baseStats?.intuition || 10
},
weaponDamage: weaponDamage + skillBonus,
attack: baseAttack + attackBonus,
critRate: playerStore.globalBonus?.critRate || 0
}
}
/**
* 根据武器获取对应的技能ID
* @param {Object} weapon - 武器对象
* @returns {String|null} 技能ID
*/
function getWeaponSkillId(weapon) {
if (weapon.id === 'wooden_stick') return 'stick_mastery'
if (weapon.subtype === 'sword') return 'sword_mastery'
if (weapon.subtype === 'axe') return 'axe_mastery'
return null
}
/**
* 处理战斗tick每秒执行一次
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} combatState - 战斗状态
* @returns {Object} { victory: boolean, defeat: boolean, continue: boolean, logs: Array }
*/
export function combatTick(gameStore, playerStore, combatState) {
const logs = []
if (!combatState || !combatState.enemy) {
return { victory: false, defeat: false, continue: false, logs: ['战斗状态异常'] }
}
const enemy = combatState.enemy
const stance = combatState.stance || 'balance'
const environment = combatState.environment || 'normal'
// 获取环境惩罚减少(夜视技能)
const darkPenaltyReduce = playerStore.globalBonus?.darkPenaltyReduce || 0
// 构建玩家战斗者对象(包含武器伤害)
const playerAttacker = buildPlayerAttacker(playerStore)
// 玩家攻击
const playerBonuses = {
skillBonus: 0, // 已在 buildPlayerAttacker 中计算
equipBonus: 0, // 已在 buildPlayerAttacker 中计算
darkPenaltyReduce
}
const playerAttack = processAttack(
playerAttacker,
enemy,
stance,
environment,
playerBonuses,
{}
)
if (playerAttack.hit) {
enemy.hp = Math.max(0, enemy.hp - playerAttack.damage)
if (playerAttack.crit) {
logs.push({
type: 'combat',
message: `你暴击了!对${enemy.name}造成${playerAttack.damage}点伤害!`,
highlight: true
})
} else {
logs.push({
type: 'combat',
message: `你攻击${enemy.name},造成${playerAttack.damage}点伤害`
})
}
if (playerAttack.shieldAbsorbed > 0) {
logs.push({
type: 'combat',
message: `${enemy.name}的护盾吸收了${playerAttack.shieldAbsorbed}点伤害`
})
}
} else if (playerAttack.evaded) {
logs.push({
type: 'combat',
message: `你攻击${enemy.name},未命中!`
})
}
// 检查敌人是否死亡
if (enemy.hp <= 0) {
return {
victory: true,
defeat: false,
continue: false,
logs,
enemy
}
}
// 敌人攻击
const enemyAttack = processAttack(
enemy,
playerStore,
'balance',
environment,
{},
playerBonuses
)
if (enemyAttack.hit) {
playerStore.currentStats.health = Math.max(
0,
playerStore.currentStats.health - enemyAttack.damage
)
logs.push({
type: 'combat',
message: `${enemy.name}攻击你,造成${enemyAttack.damage}点伤害`
})
if (enemyAttack.crit) {
logs.push({
type: 'combat',
message: `${enemy.name}的攻击是暴击!`,
highlight: true
})
}
} else if (enemyAttack.evaded) {
logs.push({
type: 'combat',
message: `${enemy.name}攻击你,你闪避了!`
})
}
// 检查玩家是否死亡
if (playerStore.currentStats.health <= 0) {
return {
victory: false,
defeat: true,
continue: false,
logs
}
}
// 消耗耐力
const staminaCost = getStaminaCost(stance)
playerStore.currentStats.stamina = Math.max(
0,
playerStore.currentStats.stamina - staminaCost
)
return {
victory: false,
defeat: false,
continue: true,
logs,
enemy
}
}
/**
* 获取姿态的耐力消耗
* @param {String} stance - 姿态
* @returns {Number} 每秒耐力消耗
*/
export function getStaminaCost(stance) {
const costs = {
attack: 2,
defense: 1,
balance: 0.5,
rapid: 3,
heavy: 2.5
}
return costs[stance] || 0.5
}
/**
* 初始化战斗状态
* @param {String} enemyId - 敌人ID
* @param {Object} enemyConfig - 敌人配置
* @param {String} environment - 环境类型
* @returns {Object} 战斗状态
*/
export function initCombat(enemyId, enemyConfig, environment = 'normal') {
return {
enemyId,
enemy: {
id: enemyId,
name: enemyConfig.name,
hp: enemyConfig.baseStats.health,
maxHp: enemyConfig.baseStats.health,
attack: enemyConfig.baseStats.attack,
defense: enemyConfig.baseStats.defense,
baseStats: enemyConfig.baseStats,
expReward: enemyConfig.expReward,
drops: enemyConfig.drops || []
},
stance: 'balance',
environment,
startTime: Date.now(),
ticks: 0
}
}
/**
* 计算姿态切换后的攻击速度修正
* @param {String} stance - 姿态
* @param {Number} baseSpeed - 基础攻击速度
* @returns {Number} 修正后的攻击速度
*/
export function getAttackSpeed(stance, baseSpeed = 1.0) {
const speedModifiers = {
attack: 1.3, // 攻击姿态: 速度 +30%
defense: 0.9, // 防御姿态: 速度 -10%
balance: 1.0,
rapid: 2.0, // 快速打击: 速度 +100%
heavy: 0.5 // 重击: 速度 -50%
}
return baseSpeed * (speedModifiers[stance] || 1.0)
}
/**
* 获取姿态显示名称
* @param {String} stance - 姿态
* @returns {String} 显示名称
*/
export function getStanceDisplayName(stance) {
const names = {
attack: '攻击',
defense: '防御',
balance: '平衡',
rapid: '快速',
heavy: '重击'
}
return names[stance] || '平衡'
}
/**
* 获取环境类型
* @param {String} locationId - 位置ID
* @returns {String} 环境类型
*/
export function getEnvironmentType(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.environment || 'normal'
}

482
utils/environmentSystem.js Normal file
View File

@@ -0,0 +1,482 @@
/**
* 环境系统 - 环境惩罚计算、被动技能获取
* Phase 6 核心系统实现
*/
import { LOCATION_CONFIG } from '@/config/locations.js'
import { SKILL_CONFIG } from '@/config/skills.js'
/**
* 环境类型枚举
*/
export const ENVIRONMENT_TYPES = {
NORMAL: 'normal', // 普通环境
DARK: 'dark', // 黑暗环境
NARROW: 'narrow', // 狭窄空间
HARSH: 'harsh' // 恶劣环境
}
/**
* 环境惩罚配置
*/
const ENVIRONMENT_PENALTIES = {
[ENVIRONMENT_TYPES.DARK]: {
apPenalty: 0.2, // AP -20%
epPenalty: 0.2, // EP -20%
readingPenalty: 0.5, // 阅读效率 -50%
adaptSkill: 'night_vision', // 适应技能
description: '黑暗'
},
[ENVIRONMENT_TYPES.NARROW]: {
apPenalty: 0, // AP 无惩罚
epPenalty: 0.3, // EP -30%
readingPenalty: 0, // 阅读无惩罚
adaptSkill: 'narrow_fighting', // 适应技能
description: '狭窄'
},
[ENVIRONMENT_TYPES.HARSH]: {
apPenalty: 0.1, // AP -10%
epPenalty: 0.1, // EP -10%
readingPenalty: 0.2, // 阅读效率 -20%
adaptSkill: 'survivalist', // 适应技能
description: '恶劣'
},
[ENVIRONMENT_TYPES.NORMAL]: {
apPenalty: 0,
epPenalty: 0,
readingPenalty: 0,
adaptSkill: null,
description: '普通'
}
}
/**
* 获取当前位置的环境惩罚
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Object} 惩罚信息
*/
export function getEnvironmentPenalty(locationId, playerStore) {
const location = LOCATION_CONFIG[locationId]
if (!location) {
return getDefaultPenalty()
}
const environmentType = location.environment || ENVIRONMENT_TYPES.NORMAL
const basePenalty = ENVIRONMENT_PENALTIES[environmentType] || ENVIRONMENT_PENALTIES[ENVIRONMENT_TYPES.NORMAL]
// 应用技能减少惩罚
const penalty = { ...basePenalty }
// 夜视技能减少黑暗惩罚
if (environmentType === ENVIRONMENT_TYPES.DARK) {
const darkPenaltyReduce = playerStore.globalBonus?.darkPenaltyReduce || 0
penalty.apPenalty = Math.max(0, penalty.apPenalty * (1 - darkPenaltyReduce / 100))
penalty.epPenalty = Math.max(0, penalty.epPenalty * (1 - darkPenaltyReduce / 100))
penalty.readingPenalty = Math.max(0, penalty.readingPenalty * (1 - darkPenaltyReduce / 100))
}
return penalty
}
/**
* 获取默认惩罚(普通环境)
*/
function getDefaultPenalty() {
return {
apPenalty: 0,
epPenalty: 0,
readingPenalty: 0,
adaptSkill: null,
description: '普通'
}
}
/**
* 计算AP环境修正
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Number} 修正倍率 (0-1)
*/
export function getAPModifier(locationId, playerStore) {
const penalty = getEnvironmentPenalty(locationId, playerStore)
return 1 - penalty.apPenalty
}
/**
* 计算EP环境修正
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Number} 修正倍率 (0-1)
*/
export function getEPModifier(locationId, playerStore) {
const penalty = getEnvironmentPenalty(locationId, playerStore)
return 1 - penalty.epPenalty
}
/**
* 计算阅读效率修正
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Number} 修正倍率 (0-1)
*/
export function getReadingModifier(locationId, playerStore) {
const penalty = getEnvironmentPenalty(locationId, playerStore)
return 1 - penalty.readingPenalty
}
/**
* 处理环境被动经验
* 在特定环境中被动获得技能经验
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {String} locationId - 位置ID
* @param {Number} deltaTime - 时间增量(秒)
* @returns {Object} 获得的经验 { skillId: exp }
*/
export function processEnvironmentExp(gameStore, playerStore, locationId, deltaTime = 1) {
const location = LOCATION_CONFIG[locationId]
if (!location) return {}
const environmentType = location.environment || ENVIRONMENT_TYPES.NORMAL
const penalty = ENVIRONMENT_PENALTIES[environmentType]
if (!penalty.adaptSkill) {
return {}
}
const adaptSkillId = penalty.adaptSkill
const skillConfig = SKILL_CONFIG[adaptSkillId]
if (!skillConfig) {
return {}
}
// 检查技能是否已解锁
if (!playerStore.skills[adaptSkillId] || !playerStore.skills[adaptSkillId].unlocked) {
// 检查解锁条件
if (skillConfig.unlockCondition) {
const condition = skillConfig.unlockCondition
if (condition.location === locationId) {
// 首次进入该环境,解锁技能
if (!playerStore.skills[adaptSkillId]) {
playerStore.skills[adaptSkillId] = {
level: 0,
exp: 0,
unlocked: true
}
if (gameStore.addLog) {
gameStore.addLog(`解锁了被动技能: ${skillConfig.name}`, 'system')
}
}
}
}
return {}
}
// 计算被动经验(每秒获得基础经验)
const baseExpRate = 0.5 // 每秒0.5经验
const expGain = baseExpRate * deltaTime
// 应用阅读速度加成
const readingSpeedBonus = playerStore.globalBonus?.readingSpeed || 1
const finalExp = expGain * readingSpeedBonus
// 添加经验
playerStore.skills[adaptSkillId].exp += finalExp
// 检查升级
const exp = playerStore.skills[adaptSkillId].exp
const maxExp = skillConfig.expPerLevel(playerStore.skills[adaptSkillId].level + 1)
if (exp >= maxExp && playerStore.skills[adaptSkillId].level < skillConfig.maxLevel) {
playerStore.skills[adaptSkillId].level++
playerStore.skills[adaptSkillId].exp -= maxExp
if (gameStore.addLog) {
gameStore.addLog(`${skillConfig.name} 提升到了 Lv.${playerStore.skills[adaptSkillId].level}!`, 'reward')
}
// 检查里程碑奖励(需要调用技能系统)
// TODO: 调用 applyMilestoneBonus
}
return { [adaptSkillId]: finalExp }
}
/**
* 检查位置解锁条件
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Object} { unlocked: boolean, reason: string }
*/
export function checkLocationUnlock(locationId, playerStore) {
const location = LOCATION_CONFIG[locationId]
if (!location) {
return { unlocked: false, reason: '位置不存在' }
}
if (!location.unlockCondition) {
return { unlocked: true, reason: '' }
}
const condition = location.unlockCondition
switch (condition.type) {
case 'kill':
// 击杀条件
const killCount = (playerStore.killCount || {})[condition.target] || 0
if (killCount < condition.count) {
return {
unlocked: false,
reason: `需要击杀 ${condition.count}${getEnemyName(condition.target)} (当前: ${killCount})`
}
}
break
case 'item':
// 物品条件
const hasItem = playerStore.inventory?.some(i => i.id === condition.item)
if (!hasItem) {
const itemName = getItemName(condition.item)
return { unlocked: false, reason: `需要 ${itemName}` }
}
break
case 'level':
// 等级条件
const playerLevel = playerStore.level?.current || 1
if (playerLevel < condition.level) {
return { unlocked: false, reason: `需要等级 ${condition.level}` }
}
break
case 'skill':
// 技能条件
const skill = playerStore.skills[condition.skillId]
if (!skill || skill.level < condition.level) {
return { unlocked: false, reason: `需要技能达到指定等级` }
}
break
case 'flag':
// 标志条件
if (!playerStore.flags?.[condition.flag]) {
return { unlocked: false, reason: '需要完成特定事件' }
}
break
}
return { unlocked: true, reason: '' }
}
/**
* 获取敌人名称(辅助函数)
*/
function getEnemyName(enemyId) {
// 简化实现,实际应该从 ENEMY_CONFIG 获取
const names = {
wild_dog: '野狗',
test_boss: '测试Boss'
}
return names[enemyId] || enemyId
}
/**
* 获取物品名称(辅助函数)
*/
function getItemName(itemId) {
// 简化实现,实际应该从 ITEM_CONFIG 获取
const names = {
basement_key: '地下室钥匙',
old_book: '破旧书籍'
}
return names[itemId] || itemId
}
/**
* 检查位置是否安全
* @param {String} locationId - 位置ID
* @returns {Boolean}
*/
export function isSafeLocation(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.type === 'safe'
}
/**
* 检查位置是否为战斗区域
* @param {String} locationId - 位置ID
* @returns {Boolean}
*/
export function isCombatLocation(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.type === 'danger' || location?.type === 'dungeon'
}
/**
* 获取位置可用的活动
* @param {String} locationId - 位置ID
* @returns {Array} 活动列表
*/
export function getLocationActivities(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.activities || []
}
/**
* 获取位置连接
* @param {String} locationId - 位置ID
* @returns {Array} 连接的位置ID列表
*/
export function getLocationConnections(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.connections || []
}
/**
* 检查位置是否可到达
* @param {String} fromId - 起始位置ID
* @param {String} toId - 目标位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Object} { reachable: boolean, reason: string }
*/
export function canTravelTo(fromId, toId, playerStore) {
const fromLocation = LOCATION_CONFIG[fromId]
if (!fromLocation) {
return { reachable: false, reason: '当前位置不存在' }
}
// 检查是否直接连接
if (!fromLocation.connections?.includes(toId)) {
return { reachable: false, reason: '无法直接到达' }
}
// 检查目标位置解锁条件
return checkLocationUnlock(toId, playerStore)
}
/**
* 计算位置转移时间
* @param {String} fromId - 起始位置ID
* @param {String} toId - 目标位置ID
* @returns {Number} 转移时间(秒)
*/
export function getTravelTime(fromId, toId) {
// 简化实现所有位置间转移时间为5秒
return 5
}
/**
* 获取环境描述
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {String} 环境描述
*/
export function getEnvironmentDescription(locationId, playerStore) {
const location = LOCATION_CONFIG[locationId]
if (!location) return ''
const penalty = getEnvironmentPenalty(locationId, playerStore)
let description = location.description || ''
// 添加环境效果描述
if (location.environment === ENVIRONMENT_TYPES.DARK) {
const hasNightVision = playerStore.skills.night_vision?.level > 0
if (!hasNightVision) {
description += ' [黑暗: AP/EP-20% 阅读效率-50%]'
} else {
const reduce = playerStore.globalBonus?.darkPenaltyReduce || 0
description += ` [黑暗: 惩罚减少${reduce}%]`
}
}
return description
}
/**
* 获取位置的可敌人列表
* @param {String} locationId - 位置ID
* @returns {Array} 敌人ID列表
*/
export function getLocationEnemies(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.enemies || []
}
/**
* 获取位置的NPC列表
* @param {String} locationId - 位置ID
* @returns {Array} NPC ID列表
*/
export function getLocationNPCs(locationId) {
const location = LOCATION_CONFIG[locationId]
return location?.npcs || []
}
/**
* 检查是否有负面状态来自环境
* @param {String} locationId - 位置ID
* @param {Object} playerStore - 玩家Store
* @returns {Array} 负面状态列表
*/
export function getEnvironmentNegativeStatus(locationId, playerStore) {
const location = LOCATION_CONFIG[locationId]
if (!location) return []
const status = []
// 黑暗环境可能造成精神压力
if (location.environment === ENVIRONMENT_TYPES.DARK) {
const hasNightVision = playerStore.skills.night_vision?.level || 0
if (hasNightVision < 5) {
status.push({
type: 'darkness',
name: '黑暗恐惧',
effect: '精神逐渐下降',
severity: hasNightVision > 0 ? 'mild' : 'moderate'
})
}
}
return status
}
/**
* 处理环境负面状态效果
* @param {Object} playerStore - 玩家Store
* @param {String} locationId - 位置ID
* @param {Number} deltaTime - 时间增量(秒)
*/
export function processEnvironmentEffects(playerStore, locationId, deltaTime = 1) {
const location = LOCATION_CONFIG[locationId]
if (!location) return
// 黑暗环境的精神压力
if (location.environment === ENVIRONMENT_TYPES.DARK) {
const nightVisionLevel = playerStore.skills.night_vision?.level || 0
// 夜视5级以下会缓慢消耗精神
if (nightVisionLevel < 5) {
const sanityDrain = 0.1 * (1 - nightVisionLevel * 0.1) * deltaTime
playerStore.currentStats.sanity = Math.max(
0,
playerStore.currentStats.sanity - sanityDrain
)
}
}
// 恶劣环境的持续影响
if (location.environment === ENVIRONMENT_TYPES.HARSH) {
const survivalistLevel = playerStore.skills.survivalist?.level || 0
// 生存技能可以减少恶劣环境影响
if (survivalistLevel < 5) {
const staminaDrain = 0.2 * (1 - survivalistLevel * 0.1) * deltaTime
playerStore.currentStats.stamina = Math.max(
0,
playerStore.currentStats.stamina - staminaDrain
)
}
}
}

549
utils/eventSystem.js Normal file
View File

@@ -0,0 +1,549 @@
/**
* 事件系统 - 事件触发检测、对话管理
* Phase 6 核心系统实现
*/
import { NPC_CONFIG } from '@/config/npcs.js'
import { EVENT_CONFIG } from '@/config/events.js'
import { LOCATION_CONFIG } from '@/config/locations.js'
import { unlockSkill } from './skillSystem.js'
import { addItemToInventory } from './itemSystem.js'
/**
* 检查并触发事件
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {String} triggerType - 触发类型 'enter' | 'action' | 'timer'
* @param {Object} context - 触发上下文
* @returns {Object|null} 触发的事件信息
*/
export function checkAndTriggerEvent(gameStore, playerStore, triggerType = 'enter', context = {}) {
// 检查首次进入事件
if (!playerStore.flags) {
playerStore.flags = {}
}
// 新游戏初始化事件
if (!playerStore.flags.introTriggered) {
playerStore.flags.introTriggered = true
triggerEvent(gameStore, 'intro', { playerStore })
return { eventId: 'intro', type: 'story' }
}
// 检查位置事件
if (triggerType === 'enter') {
const locationEvent = checkLocationEvent(gameStore, playerStore, context.locationId)
if (locationEvent) {
return locationEvent
}
}
// 检查NPC对话事件
if (triggerType === 'npc') {
const npcEvent = checkNPCDialogue(gameStore, playerStore, context.npcId)
if (npcEvent) {
return npcEvent
}
}
// 检查战斗相关事件
if (triggerType === 'combat') {
const combatEvent = checkCombatEvent(gameStore, playerStore, context)
if (combatEvent) {
return combatEvent
}
}
// 检查解锁事件
if (triggerType === 'unlock') {
const unlockEvent = checkUnlockEvent(gameStore, playerStore, context)
if (unlockEvent) {
return unlockEvent
}
}
return null
}
/**
* 检查位置事件
*/
function checkLocationEvent(gameStore, playerStore, locationId) {
const location = LOCATION_CONFIG[locationId]
if (!location) return null
// 首次进入地下室警告
if (locationId === 'basement' && !playerStore.flags.basementFirstEnter) {
playerStore.flags.basementFirstEnter = true
triggerEvent(gameStore, 'dark_warning', { locationName: location.name })
return { eventId: 'dark_warning', type: 'tips' }
}
// 首次进入野外战斗提示
if (locationId === 'wild1' && !playerStore.flags.wild1FirstEnter) {
playerStore.flags.wild1FirstEnter = true
triggerEvent(gameStore, 'first_combat', { locationName: location.name })
return { eventId: 'first_combat', type: 'tips' }
}
return null
}
/**
* 检查NPC对话事件
*/
function checkNPCDialogue(gameStore, playerStore, npcId) {
const npc = NPC_CONFIG[npcId]
if (!npc) return null
// 启动NPC对话
startNPCDialogue(gameStore, npcId, 'first')
return { eventId: `npc_${npcId}`, type: 'dialogue' }
}
/**
* 检查战斗事件
*/
function checkCombatEvent(gameStore, playerStore, context) {
// 检查Boss解锁
if (context.result === 'victory' && context.enemyId === 'wild_dog') {
const killCount = (playerStore.killCount || {})[context.enemyId] || 0
playerStore.killCount = playerStore.killCount || {}
playerStore.killCount[context.enemyId] = killCount + 1
// 击杀5只野狗解锁Boss巢
if (playerStore.killCount[context.enemyId] >= 5 && !playerStore.flags.bossUnlockTriggered) {
playerStore.flags.bossUnlockTriggered = true
triggerEvent(gameStore, 'boss_unlock', {})
return { eventId: 'boss_unlock', type: 'unlock' }
}
}
// Boss击败事件
if (context.result === 'victory' && context.enemyId === 'test_boss' && !playerStore.flags.bossDefeatTriggered) {
playerStore.flags.bossDefeatTriggered = true
triggerEvent(gameStore, 'boss_defeat', {})
return { eventId: 'boss_defeat', type: 'story' }
}
return null
}
/**
* 检查解锁事件
*/
function checkUnlockEvent(gameStore, playerStore, context) {
// 区域解锁事件
if (context.unlockType === 'location' && context.locationId === 'boss_lair') {
// 已在战斗事件中处理
}
return null
}
/**
* 触发事件
* @param {Object} gameStore - 游戏Store
* @param {String} eventId - 事件ID
* @param {Object} context - 事件上下文
*/
export function triggerEvent(gameStore, eventId, context = {}) {
const config = EVENT_CONFIG[eventId]
if (!config) {
console.warn(`Event ${eventId} not found`)
return
}
// 构建事件数据
const eventData = {
id: eventId,
type: config.type,
title: config.title || '',
text: config.text || '',
npc: config.npcId ? NPC_CONFIG[config.npcId] : null,
choices: config.choices || [],
actions: config.actions || [],
context,
triggers: config.triggers || []
}
// 处理动态文本(替换变量)
if (config.textTemplate && context) {
eventData.text = renderTemplate(config.textTemplate, context)
}
// 设置当前事件
gameStore.currentEvent = eventData
// 打开事件抽屉
gameStore.drawerState.event = true
}
/**
* 渲染模板文本
*/
function renderTemplate(template, context) {
let text = template
for (const [key, value] of Object.entries(context)) {
text = text.replace(new RegExp(`\\{${key}\\}`, 'g'), value)
}
return text
}
/**
* 启动NPC对话
* @param {Object} gameStore - 游戏Store
* @param {String} npcId - NPC ID
* @param {String} dialogueKey - 对话键名
*/
export function startNPCDialogue(gameStore, npcId, dialogueKey = 'first') {
const npc = NPC_CONFIG[npcId]
if (!npc) {
console.warn(`NPC ${npcId} not found`)
return
}
const dialogue = npc.dialogue[dialogueKey]
if (!dialogue) {
console.warn(`Dialogue ${dialogueKey} not found for NPC ${npcId}`)
return
}
// 构建事件数据
const eventData = {
id: `npc_${npcId}_${dialogueKey}`,
type: 'dialogue',
title: npc.name,
text: dialogue.text,
npc: npc,
choices: dialogue.choices || [],
currentDialogue: dialogueKey,
npcId
}
gameStore.currentEvent = eventData
gameStore.drawerState.event = true
}
/**
* 处理对话选择
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} choice - 选择项
* @returns {Object} 处理结果
*/
export function handleDialogueChoice(gameStore, playerStore, choice) {
const result = {
success: true,
closeEvent: false,
message: ''
}
// 处理对话动作
if (choice.action) {
const actionResult = processDialogueAction(gameStore, playerStore, choice.action, choice.actionData)
result.message = actionResult.message
if (actionResult.closeEvent) {
result.closeEvent = true
}
}
// 检查下一步
if (choice.next === null) {
// 对话结束
result.closeEvent = true
} else if (choice.next) {
// 继续对话
const currentEvent = gameStore.currentEvent
if (currentEvent && currentEvent.npc) {
startNPCDialogue(gameStore, currentEvent.npc.id, choice.next)
result.closeEvent = false
}
} else if (!choice.action) {
// 没有next也没有action关闭
result.closeEvent = true
}
// 如果需要关闭事件
if (result.closeEvent) {
gameStore.drawerState.event = false
gameStore.currentEvent = null
}
return result
}
/**
* 处理对话动作
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {String} action - 动作类型
* @param {Object} actionData - 动作数据
* @returns {Object} 处理结果
*/
export function processDialogueAction(gameStore, playerStore, action, actionData = {}) {
switch (action) {
case 'give_stick':
// 给予木棍
const stickResult = addItemToInventory(playerStore, 'wooden_stick', 1, 100)
if (stickResult.success) {
// 自动解锁木棍精通技能
unlockSkill(playerStore, 'stick_mastery')
if (gameStore.addLog) {
gameStore.addLog('获得了 木棍', 'reward')
}
}
return { success: true, message: '获得了木棍', closeEvent: false }
case 'open_shop':
// 打开商店
gameStore.drawerState.event = false
// TODO: 打开商店抽屉
gameStore.drawerState.shop = true
return { success: true, message: '', closeEvent: true }
case 'give_item':
// 给予物品
if (actionData.itemId) {
const itemResult = addItemToInventory(
playerStore,
actionData.itemId,
actionData.count || 1,
actionData.quality || null
)
if (itemResult.success && gameStore.addLog) {
gameStore.addLog(`获得了 ${itemResult.item.name}`, 'reward')
}
return { success: true, message: `获得了 ${actionData.itemId}`, closeEvent: false }
}
return { success: false, message: '物品参数错误', closeEvent: false }
case 'unlock_skill':
// 解锁技能
if (actionData.skillId) {
unlockSkill(playerStore, actionData.skillId)
if (gameStore.addLog) {
const skillConfig = SKILL_CONFIG[actionData.skillId]
gameStore.addLog(`解锁了技能: ${skillConfig?.name || actionData.skillId}`, 'system')
}
return { success: true, message: `解锁了技能 ${actionData.skillId}`, closeEvent: false }
}
return { success: false, message: '技能参数错误', closeEvent: false }
case 'heal':
// 治疗
if (actionData.amount) {
playerStore.currentStats.health = Math.min(
playerStore.currentStats.maxHealth,
playerStore.currentStats.health + actionData.amount
)
return { success: true, message: `恢复了${actionData.amount}生命`, closeEvent: false }
}
return { success: false, message: '治疗参数错误', closeEvent: false }
case 'restore_stamina':
// 恢复耐力
if (actionData.amount) {
playerStore.currentStats.stamina = Math.min(
playerStore.currentStats.maxStamina,
playerStore.currentStats.stamina + actionData.amount
)
return { success: true, message: `恢复了${actionData.amount}耐力`, closeEvent: false }
}
return { success: false, message: '耐力参数错误', closeEvent: false }
case 'teleport':
// 传送
if (actionData.locationId) {
playerStore.currentLocation = actionData.locationId
if (gameStore.addLog) {
const location = LOCATION_CONFIG[actionData.locationId]
gameStore.addLog(`来到了 ${location?.name || actionData.locationId}`, 'system')
}
return { success: true, message: `传送到 ${actionData.locationId}`, closeEvent: true }
}
return { success: false, message: '位置参数错误', closeEvent: false }
case 'add_currency':
// 添加货币
if (actionData.amount) {
playerStore.currency.copper += actionData.amount
if (gameStore.addLog) {
gameStore.addLog(`获得了 ${actionData.amount} 铜币`, 'reward')
}
return { success: true, message: `获得了${actionData.amount}铜币`, closeEvent: false }
}
return { success: false, message: '货币参数错误', closeEvent: false }
case 'set_flag':
// 设置标志
if (actionData.flag) {
playerStore.flags = playerStore.flags || {}
playerStore.flags[actionData.flag] = actionData.value !== undefined ? actionData.value : true
return { success: true, message: `设置标志 ${actionData.flag}`, closeEvent: false }
}
return { success: false, message: '标志参数错误', closeEvent: false }
case 'check_flag':
// 检查标志(返回结果由调用者处理)
if (actionData.flag) {
const value = playerStore.flags?.[actionData.flag]
return {
success: true,
message: '',
closeEvent: false,
flagValue: value
}
}
return { success: false, message: '标志参数错误', closeEvent: false }
case 'start_combat':
// 开始战斗
if (actionData.enemyId) {
gameStore.drawerState.event = false
// TODO: 启动战斗
return { success: true, message: '开始战斗', closeEvent: true }
}
return { success: false, message: '敌人参数错误', closeEvent: false }
case 'close':
// 直接关闭
return { success: true, message: '', closeEvent: true }
default:
console.warn(`Unknown dialogue action: ${action}`)
return { success: false, message: '未知动作', closeEvent: false }
}
}
/**
* 处理事件选择
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Number} choiceIndex - 选择索引
* @returns {Object} 处理结果
*/
export function handleEventChoice(gameStore, playerStore, choiceIndex) {
const currentEvent = gameStore.currentEvent
if (!currentEvent || !currentEvent.choices || !currentEvent.choices[choiceIndex]) {
return { success: false, message: '选择无效' }
}
const choice = currentEvent.choices[choiceIndex]
return handleDialogueChoice(gameStore, playerStore, choice)
}
/**
* 关闭当前事件
* @param {Object} gameStore - 游戏Store
*/
export function closeEvent(gameStore) {
gameStore.drawerState.event = false
gameStore.currentEvent = null
}
/**
* 获取当前事件信息
* @param {Object} gameStore - 游戏Store
* @returns {Object|null} 当前事件
*/
export function getCurrentEvent(gameStore) {
return gameStore.currentEvent
}
/**
* 检查NPC是否可用
* @param {String} npcId - NPC ID
* @param {Object} playerStore - 玩家Store
* @returns {Boolean}
*/
export function isNPCAvailable(npcId, playerStore) {
const npc = NPC_CONFIG[npcId]
if (!npc) return false
// 检查位置
if (npc.location && playerStore.currentLocation !== npc.location) {
return false
}
// 检查解锁条件
if (npc.unlockCondition) {
const condition = npc.unlockCondition
if (condition.flag && !playerStore.flags?.[condition.flag]) {
return false
}
if (condition.item && !playerStore.inventory.some(i => i.id === condition.item)) {
return false
}
}
return true
}
/**
* 获取当前位置可用的NPC列表
* @param {Object} playerStore - 玩家Store
* @returns {Array} NPC ID列表
*/
export function getAvailableNPCs(playerStore) {
const location = LOCATION_CONFIG[playerStore.currentLocation]
if (!location || !location.npcs) {
return []
}
return location.npcs.filter(npcId => isNPCAvailable(npcId, playerStore))
}
/**
* 设置定时事件
* @param {Object} gameStore - 游戏Store
* @param {String} eventId - 事件ID
* @param {Number} delayMs - 延迟时间(毫秒)
*/
export function scheduleEvent(gameStore, eventId, delayMs) {
if (!gameStore.scheduledEvents) {
gameStore.scheduledEvents = []
}
gameStore.scheduledEvents.push({
eventId,
triggerTime: Date.now() + delayMs,
triggered: false
})
}
/**
* 检查并触发定时事件
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {Array} 触发的事件列表
*/
export function checkScheduledEvents(gameStore, playerStore) {
if (!gameStore.scheduledEvents) {
return []
}
const now = Date.now()
const triggeredEvents = []
for (const event of gameStore.scheduledEvents) {
if (!event.triggered && now >= event.triggerTime) {
event.triggered = true
triggerEvent(gameStore, event.eventId, { playerStore })
triggeredEvents.push(event.eventId)
}
}
// 清理已触发的事件
gameStore.scheduledEvents = gameStore.scheduledEvents.filter(e => !e.triggered)
return triggeredEvents
}
/**
* 清除所有定时事件
* @param {Object} gameStore - 游戏Store
*/
export function clearScheduledEvents(gameStore) {
gameStore.scheduledEvents = []
}

549
utils/gameLoop.js Normal file
View 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
}

569
utils/itemSystem.js Normal file
View File

@@ -0,0 +1,569 @@
/**
* 物品系统 - 使用/装备/品质计算
* Phase 6 核心系统实现
*/
import { ITEM_CONFIG } from '@/config/items.js'
import { GAME_CONSTANTS } from '@/config/constants.js'
import { SKILL_CONFIG } from '@/config/skills.js'
/**
* 计算装备品质后的属性
* @param {String} itemId - 物品ID
* @param {Number} quality - 品质值 (0-250)
* @returns {Object|null} 计算后的属性
*/
export function calculateItemStats(itemId, quality = 100) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return null
}
const qualityLevel = getQualityLevel(quality)
const qualityMultiplier = qualityLevel.multiplier
const stats = {
id: itemId,
name: config.name,
type: config.type,
subtype: config.subtype,
description: config.description,
icon: config.icon,
quality: quality,
qualityLevel: qualityLevel.level,
qualityName: qualityLevel.name,
qualityColor: qualityLevel.color
}
// 应用品质百分比到基础属性
const qualityRatio = quality / 100
if (config.baseDamage) {
stats.baseDamage = config.baseDamage
stats.finalDamage = Math.floor(config.baseDamage * qualityRatio * qualityMultiplier)
}
if (config.baseDefense) {
stats.baseDefense = config.baseDefense
stats.finalDefense = Math.floor(config.baseDefense * qualityRatio * qualityMultiplier)
}
if (config.baseShield) {
stats.baseShield = config.baseShield
stats.finalShield = Math.floor(config.baseShield * qualityRatio * qualityMultiplier)
}
if (config.attackSpeed) {
stats.attackSpeed = config.attackSpeed
}
// 计算价值
stats.baseValue = config.baseValue || 0
stats.finalValue = Math.floor(config.baseValue * qualityRatio * qualityMultiplier)
// 消耗品效果
if (config.effect) {
stats.effect = { ...config.effect }
}
// 书籍信息
if (config.type === 'book') {
stats.readingTime = config.readingTime
stats.expReward = { ...config.expReward }
stats.completionBonus = config.completionBonus
}
// 解锁技能
if (config.unlockSkill) {
stats.unlockSkill = config.unlockSkill
}
// 堆叠信息
if (config.stackable !== undefined) {
stats.stackable = config.stackable
stats.maxStack = config.maxStack || 99
}
return stats
}
/**
* 获取品质等级
* @param {Number} quality - 品质值
* @returns {Object} 品质等级信息
*/
export function getQualityLevel(quality) {
// 确保品质在有效范围内
const clampedQuality = Math.max(0, Math.min(250, quality))
for (const [level, data] of Object.entries(GAME_CONSTANTS.QUALITY_LEVELS)) {
if (clampedQuality >= data.range[0] && clampedQuality <= data.range[1]) {
return {
level: parseInt(level),
...data
}
}
}
// 默认返回垃圾品质
return GAME_CONSTANTS.QUALITY_LEVELS[1]
}
/**
* 生成随机品质
* @param {Number} luck - 运气值 (0-100),影响高品质概率
* @returns {Number} 品质值
*/
export function generateRandomQuality(luck = 0) {
// 基础品质范围 50-150
const baseMin = 50
const baseMax = 150
// 运气加成每1点运气增加0.5品质
const luckBonus = luck * 0.5
// 随机品质
let quality = Math.floor(Math.random() * (baseMax - baseMin + 1)) + baseMin + luckBonus
// 小概率生成高品质(传说)
if (Math.random() < 0.01 + luck / 10000) {
quality = 180 + Math.floor(Math.random() * 70) // 180-250
}
// 史诗品质
else if (Math.random() < 0.05 + luck / 2000) {
quality = 160 + Math.floor(Math.random() * 40) // 160-199
}
// 稀有品质
else if (Math.random() < 0.15 + luck / 500) {
quality = 130 + Math.floor(Math.random() * 30) // 130-159
}
// 优秀品质
else if (Math.random() < 0.3 + luck / 200) {
quality = 100 + Math.floor(Math.random() * 30) // 100-129
}
return Math.min(250, Math.max(0, Math.floor(quality)))
}
/**
* 使用物品
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {String} itemId - 物品ID
* @param {Number} count - 使用数量
* @returns {Object} { success: boolean, message: string, effects: Object }
*/
export function useItem(playerStore, gameStore, itemId, count = 1) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return { success: false, message: '物品不存在' }
}
// 查找背包中的物品
const itemIndex = playerStore.inventory.findIndex(i => i.id === itemId)
if (itemIndex === -1) {
return { success: false, message: '背包中没有该物品' }
}
const inventoryItem = playerStore.inventory[itemIndex]
// 检查数量
if (inventoryItem.count < count) {
return { success: false, message: '物品数量不足' }
}
// 根据物品类型处理
if (config.type === 'consumable') {
return useConsumable(playerStore, gameStore, config, inventoryItem, count, itemIndex)
}
if (config.type === 'book') {
return useBook(playerStore, gameStore, config, inventoryItem)
}
return { success: false, message: '该物品无法使用' }
}
/**
* 使用消耗品
*/
function useConsumable(playerStore, gameStore, config, inventoryItem, count, itemIndex) {
const effects = {}
const appliedEffects = []
// 应用效果
if (config.effect) {
if (config.effect.health) {
const oldHealth = playerStore.currentStats.health
playerStore.currentStats.health = Math.min(
playerStore.currentStats.maxHealth,
playerStore.currentStats.health + config.effect.health * count
)
effects.health = playerStore.currentStats.health - oldHealth
if (effects.health > 0) appliedEffects.push(`恢复${effects.health}生命`)
}
if (config.effect.stamina) {
const oldStamina = playerStore.currentStats.stamina
playerStore.currentStats.stamina = Math.min(
playerStore.currentStats.maxStamina,
playerStore.currentStats.stamina + config.effect.stamina * count
)
effects.stamina = playerStore.currentStats.stamina - oldStamina
if (effects.stamina > 0) appliedEffects.push(`恢复${effects.stamina}耐力`)
}
if (config.effect.sanity) {
const oldSanity = playerStore.currentStats.sanity
playerStore.currentStats.sanity = Math.min(
playerStore.currentStats.maxSanity,
playerStore.currentStats.sanity + config.effect.sanity * count
)
effects.sanity = playerStore.currentStats.sanity - oldSanity
if (effects.sanity > 0) appliedEffects.push(`恢复${effects.sanity}精神`)
}
}
// 消耗物品
inventoryItem.count -= count
if (inventoryItem.count <= 0) {
playerStore.inventory.splice(itemIndex, 1)
}
// 记录日志
const effectText = appliedEffects.length > 0 ? appliedEffects.join('、') : '使用'
if (gameStore.addLog) {
gameStore.addLog(`使用了 ${config.name} x${count}${effectText}`, 'info')
}
return {
success: true,
message: effectText,
effects
}
}
/**
* 使用书籍(阅读)
*/
function useBook(playerStore, gameStore, config, inventoryItem) {
// 书籍不消耗,返回阅读任务信息
return {
success: true,
message: '开始阅读',
readingTask: {
itemId: config.id,
bookName: config.name,
readingTime: config.readingTime,
expReward: config.expReward,
completionBonus: config.completionBonus
}
}
}
/**
* 装备物品
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {String} uniqueId - 物品唯一ID背包中的物品
* @returns {Object} { success: boolean, message: string }
*/
export function equipItem(playerStore, gameStore, uniqueId) {
const inventoryItem = playerStore.inventory.find(i => i.uniqueId === uniqueId)
if (!inventoryItem) {
return { success: false, message: '物品不存在' }
}
const config = ITEM_CONFIG[inventoryItem.id]
if (!config) {
return { success: false, message: '物品配置不存在' }
}
// 确定装备槽位
let slot = config.type
if (config.subtype === 'one_handed' || config.subtype === 'two_handed') {
slot = 'weapon'
} else if (config.type === 'armor') {
slot = 'armor'
} else if (config.type === 'shield') {
slot = 'shield'
} else if (config.type === 'accessory') {
slot = 'accessory'
}
if (!slot || !playerStore.equipment.hasOwnProperty(slot)) {
return { success: false, message: '无法装备该物品' }
}
// 如果已有装备,先卸下
const currentlyEquipped = playerStore.equipment[slot]
if (currentlyEquipped) {
unequipItemBySlot(playerStore, gameStore, slot)
}
// 装备新物品
playerStore.equipment[slot] = inventoryItem
inventoryItem.equipped = true
// 应用装备属性
applyEquipmentStats(playerStore, slot, inventoryItem)
// 记录日志
if (gameStore.addLog) {
gameStore.addLog(`装备了 ${inventoryItem.name}`, 'info')
}
return { success: true, message: '装备成功' }
}
/**
* 卸下装备
* @param {Object} playerStore - 玩家Store
* @param {Object} gameStore - 游戏Store
* @param {String} slot - 装备槽位
* @returns {Object} { success: boolean, message: string }
*/
export function unequipItemBySlot(playerStore, gameStore, slot) {
const equipped = playerStore.equipment[slot]
if (!equipped) {
return { success: false, message: '该槽位没有装备' }
}
// 移除装备属性
removeEquipmentStats(playerStore, slot)
// 标记为未装备
equipped.equipped = false
// 清空槽位
playerStore.equipment[slot] = null
// 记录日志
if (gameStore.addLog) {
gameStore.addLog(`卸下了 ${equipped.name}`, 'info')
}
return { success: true, message: '卸下成功' }
}
/**
* 应用装备属性到玩家
*/
function applyEquipmentStats(playerStore, slot, item) {
if (!playerStore.equipmentStats) {
playerStore.equipmentStats = {
attack: 0,
defense: 0,
shield: 0,
critRate: 0,
attackSpeed: 1
}
}
if (item.finalDamage) {
playerStore.equipmentStats.attack += item.finalDamage
}
if (item.finalDefense) {
playerStore.equipmentStats.defense += item.finalDefense
}
if (item.finalShield) {
playerStore.equipmentStats.shield += item.finalShield
}
if (item.attackSpeed) {
playerStore.equipmentStats.attackSpeed *= item.attackSpeed
}
}
/**
* 移除装备属性
*/
function removeEquipmentStats(playerStore, slot) {
const equipped = playerStore.equipment[slot]
if (!equipped || !playerStore.equipmentStats) {
return
}
if (equipped.finalDamage) {
playerStore.equipmentStats.attack -= equipped.finalDamage
}
if (equipped.finalDefense) {
playerStore.equipmentStats.defense -= equipped.finalDefense
}
if (equipped.finalShield) {
playerStore.equipmentStats.shield -= equipped.finalShield
}
}
/**
* 添加物品到背包
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @param {Number} count - 数量
* @param {Number} quality - 品质(装备用)
* @returns {Object} { success: boolean, item: Object }
*/
export function addItemToInventory(playerStore, itemId, count = 1, quality = null) {
const config = ITEM_CONFIG[itemId]
if (!config) {
return { success: false, message: '物品不存在' }
}
// 素材和关键道具不需要品质,使用固定值
// 装备类物品才需要随机品质
const needsQuality = config.type === 'weapon' || config.type === 'armor' ||
config.type === 'shield' || config.type === 'accessory'
const itemQuality = quality !== null ? quality
: (needsQuality ? generateRandomQuality(playerStore.baseStats?.intuition || 0) : 100)
// 计算物品属性
const itemStats = calculateItemStats(itemId, itemQuality)
// 堆叠物品 - 只需要匹配ID不需要匹配品质素材类物品
if (config.stackable) {
const existingItem = playerStore.inventory.find(i => i.id === itemId)
if (existingItem) {
existingItem.count += count
return { success: true, item: existingItem }
}
}
// 创建新物品
const newItem = {
...itemStats,
uniqueId: `${itemId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
count,
equipped: false,
obtainedAt: Date.now()
}
playerStore.inventory.push(newItem)
return { success: true, item: newItem }
}
/**
* 从背包移除物品
* @param {Object} playerStore - 玩家Store
* @param {String} uniqueId - 物品唯一ID
* @param {Number} count - 移除数量
* @returns {Object} { success: boolean, message: string }
*/
export function removeItemFromInventory(playerStore, uniqueId, count = 1) {
const itemIndex = playerStore.inventory.findIndex(i => i.uniqueId === uniqueId)
if (itemIndex === -1) {
return { success: false, message: '物品不存在' }
}
const item = playerStore.inventory[itemIndex]
if (item.count < count) {
return { success: false, message: '物品数量不足' }
}
item.count -= count
if (item.count <= 0) {
// 如果已装备,先卸下
if (item.equipped) {
for (const [slot, equipped] of Object.entries(playerStore.equipment)) {
if (equipped && equipped.uniqueId === uniqueId) {
playerStore.equipment[slot] = null
break
}
}
}
playerStore.inventory.splice(itemIndex, 1)
}
return { success: true, message: '移除成功' }
}
/**
* 检查是否拥有物品
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @returns {Boolean}
*/
export function hasItem(playerStore, itemId) {
return playerStore.inventory.some(i => i.id === itemId && i.count > 0)
}
/**
* 获取物品数量
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @returns {Number}
*/
export function getItemCount(playerStore, itemId) {
const item = playerStore.inventory.find(i => i.id === itemId)
return item ? item.count : 0
}
/**
* 检查装备是否解锁技能
* @param {Object} playerStore - 玩家Store
* @param {String} itemId - 物品ID
* @returns {String|null} 解锁的技能ID
*/
export function checkSkillUnlock(playerStore, itemId) {
const config = ITEM_CONFIG[itemId]
if (!config || !config.unlockSkill) {
return null
}
const skillId = config.unlockSkill
// 检查技能是否已解锁
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
return null
}
return skillId
}
/**
* 计算物品出售价格
* @param {Object} item - 物品对象
* @param {Number} marketRate - 市场价格倍率 (0.1-2.0)
* @returns {Number} 出售价格(铜币)
*/
export function calculateSellPrice(item, marketRate = 0.3) {
const basePrice = item.finalValue || item.baseValue || 0
return Math.max(1, Math.floor(basePrice * marketRate))
}
/**
* 计算物品购买价格
* @param {Object} item - 物品对象
* @param {Number} marketRate - 市场价格倍率 (1.0-3.0)
* @returns {Number} 购买价格(铜币)
*/
export function calculateBuyPrice(item, marketRate = 2.0) {
const basePrice = item.finalValue || item.baseValue || 0
return Math.max(1, Math.floor(basePrice * marketRate))
}
/**
* 获取品质颜色
* @param {Number} quality - 品质值
* @returns {String} 颜色代码
*/
export function getQualityColor(quality) {
const level = getQualityLevel(quality)
return level.color
}
/**
* 获取品质名称
* @param {Number} quality - 品质值
* @returns {String} 品质名称
*/
export function getQualityName(quality) {
const level = getQualityLevel(quality)
return level.name
}

449
utils/skillSystem.js Normal file
View File

@@ -0,0 +1,449 @@
/**
* 技能系统 - 技能经验、升级、里程碑、父技能同步
* Phase 6 核心系统实现
*/
import { SKILL_CONFIG } from '@/config/skills.js'
import { GAME_CONSTANTS } from '@/config/constants.js'
/**
* 添加技能经验
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @param {Number} amount - 经验值
* @param {Object} options - 选项 { partialSuccess: boolean }
* @returns {Object} { leveledUp: boolean, newLevel: number, milestoneReached: number|null }
*/
export function addSkillExp(playerStore, skillId, amount, options = {}) {
const { partialSuccess = false } = options
// 检查技能是否存在且已解锁
if (!playerStore.skills || !playerStore.skills[skillId]) {
return { leveledUp: false, newLevel: 0, milestoneReached: null }
}
const skill = playerStore.skills[skillId]
if (!skill.unlocked) {
return { leveledUp: false, newLevel: 0, milestoneReached: null }
}
// 部分成功时经验减半
const finalAmount = partialSuccess ? Math.floor(amount * GAME_CONSTANTS.EXP_PARTIAL_SUCCESS) : amount
// 检查玩家等级限制
const maxSkillLevel = playerStore.level ? playerStore.level.current * 2 : 2
if (skill.level >= maxSkillLevel) {
return { leveledUp: false, newLevel: skill.level, milestoneReached: null, capped: true }
}
// 获取技能配置
const config = SKILL_CONFIG[skillId]
if (!config) {
return { leveledUp: false, newLevel: 0, milestoneReached: null }
}
// 计算有效等级上限(取技能上限和玩家限制的最小值)
const effectiveMaxLevel = Math.min(config.maxLevel, maxSkillLevel)
// 添加经验
skill.exp += finalAmount
let leveledUp = false
let milestoneReached = null
// 检查升级(可能连续升级)
while (skill.level < effectiveMaxLevel && skill.exp >= getNextLevelExp(skillId, skill.level)) {
const result = levelUpSkill(playerStore, skillId, skill.level)
leveledUp = true
if (result.milestone) {
milestoneReached = result.milestone
}
}
return {
leveledUp,
newLevel: skill.level,
milestoneReached,
capped: skill.level >= effectiveMaxLevel
}
}
/**
* 获取下一级所需经验
* @param {String} skillId - 技能ID
* @param {Number} currentLevel - 当前等级
* @returns {Number} 所需经验值
*/
export function getNextLevelExp(skillId, currentLevel) {
const config = SKILL_CONFIG[skillId]
if (!config) return 100
// 使用配置中的经验公式,默认为 level * 100
if (typeof config.expPerLevel === 'function') {
return config.expPerLevel(currentLevel + 1)
}
return (currentLevel + 1) * 100
}
/**
* 技能升级
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @param {Number} currentLevel - 当前等级
* @returns {Object} { milestone: number|null, effects: Object }
*/
export function levelUpSkill(playerStore, skillId, currentLevel) {
const skill = playerStore.skills[skillId]
const config = SKILL_CONFIG[skillId]
if (!skill || !config) {
return { milestone: null, effects: {} }
}
// 增加等级
skill.level++
const requiredExp = getNextLevelExp(skillId, currentLevel)
skill.exp -= requiredExp
// 检查里程碑奖励
let milestone = null
let effects = {}
if (config.milestones && config.milestones[skill.level]) {
milestone = skill.level
effects = applyMilestoneBonus(playerStore, skillId, skill.level)
}
// 更新父技能等级
updateParentSkill(playerStore, skillId)
return { milestone, effects }
}
/**
* 应用里程碑奖励
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @param {Number} level - 里程碑等级
* @returns {Object} 应用的效果
*/
export function applyMilestoneBonus(playerStore, skillId, level) {
const config = SKILL_CONFIG[skillId]
if (!config || !config.milestones || !config.milestones[level]) {
return {}
}
const milestone = config.milestones[level]
const effects = { ...milestone.effect }
// 初始化里程碑奖励存储
if (!playerStore.milestoneBonuses) {
playerStore.milestoneBonuses = {}
}
const bonusKey = `${skillId}_${level}`
// 如果已经应用过,跳过
if (playerStore.milestoneBonuses[bonusKey]) {
return effects
}
// 标记里程碑已应用
playerStore.milestoneBonuses[bonusKey] = {
skillId,
level,
desc: milestone.desc,
effect: effects,
appliedAt: Date.now()
}
// 应用全局属性加成
if (!playerStore.globalBonus) {
playerStore.globalBonus = {
critRate: 0,
attackBonus: 0,
expRate: 1,
critMult: 0,
darkPenaltyReduce: 0,
globalExpRate: 0,
readingSpeed: 1
}
}
// 应用具体效果
if (effects.critRate) {
playerStore.globalBonus.critRate += effects.critRate
}
if (effects.attackBonus) {
playerStore.globalBonus.attackBonus += effects.attackBonus
}
if (effects.expRate) {
playerStore.globalBonus.expRate *= effects.expRate
}
if (effects.critMult) {
playerStore.globalBonus.critMult += effects.critMult
}
if (effects.darkPenaltyReduce) {
playerStore.globalBonus.darkPenaltyReduce += effects.darkPenaltyReduce
}
if (effects.globalExpRate) {
playerStore.globalBonus.globalExpRate += effects.globalExpRate
}
if (effects.readingSpeed) {
playerStore.globalBonus.readingSpeed *= effects.readingSpeed
}
return effects
}
/**
* 更新父技能等级
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 子技能ID
*/
export function updateParentSkill(playerStore, skillId) {
const config = SKILL_CONFIG[skillId]
if (!config || !config.parentSkill) {
return
}
const parentSkillId = config.parentSkill
// 查找所有子技能的最高等级
const childSkills = Object.keys(SKILL_CONFIG).filter(id =>
SKILL_CONFIG[id].parentSkill === parentSkillId
)
let maxChildLevel = 0
for (const childId of childSkills) {
const childSkill = playerStore.skills[childId]
if (childSkill && childSkill.unlocked) {
maxChildLevel = Math.max(maxChildLevel, childSkill.level)
}
}
// 更新父技能等级
if (!playerStore.skills[parentSkillId]) {
playerStore.skills[parentSkillId] = {
level: 0,
exp: 0,
unlocked: true
}
}
playerStore.skills[parentSkillId].level = maxChildLevel
}
/**
* 解锁技能
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @returns {boolean} 是否成功解锁
*/
export function unlockSkill(playerStore, skillId) {
if (!playerStore.skills) {
playerStore.skills = {}
}
const config = SKILL_CONFIG[skillId]
if (!config) {
return false
}
// 检查是否已解锁
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
return true
}
// 初始化技能数据
playerStore.skills[skillId] = {
level: 0,
exp: 0,
unlocked: true,
maxExp: getNextLevelExp(skillId, 0)
}
return true
}
/**
* 检查技能是否可解锁
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @returns {Object} { canUnlock: boolean, reason: string }
*/
export function canUnlockSkill(playerStore, skillId) {
const config = SKILL_CONFIG[skillId]
if (!config) {
return { canUnlock: false, reason: '技能不存在' }
}
// 已解锁
if (playerStore.skills[skillId] && playerStore.skills[skillId].unlocked) {
return { canUnlock: true, reason: '已解锁' }
}
// 检查解锁条件
if (config.unlockCondition) {
const condition = config.unlockCondition
// 物品条件
if (condition.type === 'item') {
const hasItem = playerStore.inventory?.some(i => i.id === condition.item)
if (!hasItem) {
return { canUnlock: false, reason: '需要特定物品' }
}
}
// 位置条件
if (condition.location) {
if (playerStore.currentLocation !== condition.location) {
return { canUnlock: false, reason: '需要在特定位置解锁' }
}
}
// 技能等级条件
if (condition.skillLevel) {
const requiredSkill = playerStore.skills[condition.skillId]
if (!requiredSkill || requiredSkill.level < condition.skillLevel) {
return { canUnlock: false, reason: '需要前置技能达到特定等级' }
}
}
}
return { canUnlock: true, reason: '' }
}
/**
* 获取技能经验倍率(含父技能加成)
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @returns {Number} 经验倍率
*/
export function getSkillExpRate(playerStore, skillId) {
let rate = 1.0
// 全局经验加成
if (playerStore.globalBonus && playerStore.globalBonus.globalExpRate) {
rate *= (1 + playerStore.globalBonus.globalExpRate / 100)
}
// 父技能加成
const config = SKILL_CONFIG[skillId]
if (config && config.parentSkill) {
const parentSkill = playerStore.skills[config.parentSkill]
if (parentSkill && parentSkill.unlocked) {
const parentConfig = SKILL_CONFIG[config.parentSkill]
if (parentConfig && parentConfig.milestones) {
// 检查父技能里程碑奖励
for (const [level, milestone] of Object.entries(parentConfig.milestones)) {
if (parentSkill.level >= parseInt(level) && milestone.effect?.expRate) {
rate *= milestone.effect.expRate
}
}
}
// 如果子技能等级低于父技能,提供额外加成
if (parentSkill.level > 0 && playerStore.skills[skillId]) {
const childLevel = playerStore.skills[skillId].level
if (childLevel < parentSkill.level) {
rate *= 1.5 // 父技能倍率加成
}
}
}
}
return rate
}
/**
* 获取技能显示信息
* @param {Object} playerStore - 玩家Store
* @param {String} skillId - 技能ID
* @returns {Object|null} 技能显示信息
*/
export function getSkillDisplayInfo(playerStore, skillId) {
const config = SKILL_CONFIG[skillId]
if (!config) {
return null
}
const skillData = playerStore.skills[skillId] || {
level: 0,
exp: 0,
unlocked: false
}
const maxExp = getNextLevelExp(skillId, skillData.level)
const progress = skillData.level >= config.maxLevel ? 1 : skillData.exp / maxExp
// 获取已达到的里程碑
const reachedMilestones = []
if (config.milestones && playerStore.milestoneBonuses) {
for (const [level, milestone] of Object.entries(config.milestones)) {
const bonusKey = `${skillId}_${level}`
if (playerStore.milestoneBonuses[bonusKey]) {
reachedMilestones.push({
level: parseInt(level),
desc: milestone.desc
})
}
}
}
return {
id: skillId,
name: config.name,
type: config.type,
category: config.category,
icon: config.icon,
level: skillData.level,
maxLevel: config.maxLevel,
exp: skillData.exp,
maxExp,
progress,
unlocked: skillData.unlocked,
reachedMilestones,
nextMilestone: getNextMilestone(config, skillData.level)
}
}
/**
* 获取下一个里程碑
* @param {Object} config - 技能配置
* @param {Number} currentLevel - 当前等级
* @returns {Object|null} 下一个里程碑
*/
function getNextMilestone(config, currentLevel) {
if (!config.milestones) {
return null
}
for (const [level, milestone] of Object.entries(config.milestones)) {
if (parseInt(level) > currentLevel) {
return {
level: parseInt(level),
desc: milestone.desc
}
}
}
return null
}
/**
* 重置技能系统(用于新游戏)
* @param {Object} playerStore - 玩家Store
*/
export function resetSkillSystem(playerStore) {
playerStore.skills = {}
playerStore.milestoneBonuses = {}
playerStore.globalBonus = {
critRate: 0,
attackBonus: 0,
expRate: 1,
critMult: 0,
darkPenaltyReduce: 0,
globalExpRate: 0,
readingSpeed: 1
}
}

464
utils/storage.js Normal file
View File

@@ -0,0 +1,464 @@
/**
* 存档系统 - 保存、加载、重置游戏数据
* Phase 7 核心实现
*/
import { resetSkillSystem } from './skillSystem.js'
// 存档版本(用于版本控制和迁移)
const SAVE_VERSION = 1
const SAVE_KEY = 'wwa3_save_data'
/**
* 保存游戏数据
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {boolean} 是否保存成功
*/
export function saveGame(gameStore, playerStore) {
try {
// 构建存档数据
const saveData = {
version: SAVE_VERSION,
timestamp: Date.now(),
player: serializePlayerData(playerStore),
game: serializeGameData(gameStore)
}
// 使用uni.setStorageSync同步保存
uni.setStorageSync(SAVE_KEY, JSON.stringify(saveData))
console.log('Game saved successfully')
return true
} catch (error) {
console.error('Failed to save game:', error)
return false
}
}
/**
* 加载游戏数据
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {Object} { success: boolean, isNewGame: boolean, version: number }
*/
export function loadGame(gameStore, playerStore) {
try {
// 读取存档
const saveDataRaw = uni.getStorageSync(SAVE_KEY)
if (!saveDataRaw) {
console.log('No save data found, starting new game')
return { success: false, isNewGame: true, version: 0 }
}
const saveData = JSON.parse(saveDataRaw)
// 验证存档版本
if (saveData.version > SAVE_VERSION) {
console.warn('Save version is newer than game version')
return { success: false, isNewGame: false, error: 'version_mismatch' }
}
// 应用存档数据
applySaveData(gameStore, playerStore, saveData)
console.log(`Game loaded successfully (version ${saveData.version})`)
return { success: true, isNewGame: false, version: saveData.version }
} catch (error) {
console.error('Failed to load game:', error)
return { success: false, isNewGame: true, error: error.message }
}
}
/**
* 重置游戏数据
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {boolean} 是否重置成功
*/
export function resetGame(gameStore, playerStore) {
try {
// 重置玩家数据
playerStore.$reset?.() || resetPlayerData(playerStore)
// 重置游戏数据
gameStore.$reset?.() || resetGameData(gameStore)
// 删除存档
uni.removeStorageSync(SAVE_KEY)
console.log('Game reset successfully')
return true
} catch (error) {
console.error('Failed to reset game:', error)
return false
}
}
/**
* 应用存档数据到Store
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Object} saveData - 存档数据
*/
export function applySaveData(gameStore, playerStore, saveData) {
// 应用玩家数据
if (saveData.player) {
applyPlayerData(playerStore, saveData.player)
}
// 应用游戏数据
if (saveData.game) {
applyGameData(gameStore, saveData.game)
}
// 版本迁移(如果需要)
if (saveData.version < SAVE_VERSION) {
migrateSaveData(saveData.version, SAVE_VERSION, gameStore, playerStore)
}
}
/**
* 序列化玩家数据
* @param {Object} playerStore - 玩家Store
* @returns {Object} 序列化的数据
*/
function serializePlayerData(playerStore) {
return {
baseStats: { ...playerStore.baseStats },
currentStats: { ...playerStore.currentStats },
level: { ...playerStore.level },
skills: JSON.parse(JSON.stringify(playerStore.skills || {})),
equipment: JSON.parse(JSON.stringify(playerStore.equipment || {})),
inventory: JSON.parse(JSON.stringify(playerStore.inventory || [])),
currency: { ...playerStore.currency },
currentLocation: playerStore.currentLocation,
flags: { ...playerStore.flags },
// 额外数据
milestoneBonuses: playerStore.milestoneBonuses ? JSON.parse(JSON.stringify(playerStore.milestoneBonuses)) : {},
globalBonus: playerStore.globalBonus ? { ...playerStore.globalBonus } : null,
equipmentStats: playerStore.equipmentStats ? { ...playerStore.equipmentStats } : null,
killCount: playerStore.killCount ? { ...playerStore.killCount } : {}
}
}
/**
* 序列化游戏数据
* @param {Object} gameStore - 游戏Store
* @returns {Object} 序列化的数据
*/
function serializeGameData(gameStore) {
return {
currentTab: gameStore.currentTab,
gameTime: { ...gameStore.gameTime },
activeTasks: JSON.parse(JSON.stringify(gameStore.activeTasks || [])),
negativeStatus: JSON.parse(JSON.stringify(gameStore.negativeStatus || [])),
marketPrices: JSON.parse(JSON.stringify(gameStore.marketPrices || {})),
// 不保存日志logs不持久化
// 不保存战斗状态(重新登录时退出战斗)
// 不保存当前事件(重新登录时清除)
scheduledEvents: JSON.parse(JSON.stringify(gameStore.scheduledEvents || []))
}
}
/**
* 应用玩家数据
* @param {Object} playerStore - 玩家Store
* @param {Object} data - 序列化的玩家数据
*/
function applyPlayerData(playerStore, data) {
// 基础属性
if (data.baseStats) {
Object.assign(playerStore.baseStats, data.baseStats)
}
// 当前资源
if (data.currentStats) {
Object.assign(playerStore.currentStats, data.currentStats)
}
// 等级
if (data.level) {
Object.assign(playerStore.level, data.level)
}
// 技能
if (data.skills) {
playerStore.skills = JSON.parse(JSON.stringify(data.skills))
}
// 装备
if (data.equipment) {
playerStore.equipment = JSON.parse(JSON.stringify(data.equipment))
}
// 背包
if (data.inventory) {
playerStore.inventory = JSON.parse(JSON.stringify(data.inventory))
}
// 货币
if (data.currency) {
Object.assign(playerStore.currency, data.currency)
}
// 位置
if (data.currentLocation !== undefined) {
playerStore.currentLocation = data.currentLocation
}
// 标志
if (data.flags) {
Object.assign(playerStore.flags, data.flags)
}
// 额外数据
if (data.milestoneBonuses) {
playerStore.milestoneBonuses = JSON.parse(JSON.stringify(data.milestoneBonuses))
}
if (data.globalBonus) {
playerStore.globalBonus = { ...data.globalBonus }
}
if (data.equipmentStats) {
playerStore.equipmentStats = { ...data.equipmentStats }
}
if (data.killCount) {
playerStore.killCount = { ...data.killCount }
}
}
/**
* 应用游戏数据
* @param {Object} gameStore - 游戏Store
* @param {Object} data - 序列化的游戏数据
*/
function applyGameData(gameStore, data) {
// 当前标签
if (data.currentTab !== undefined) {
gameStore.currentTab = data.currentTab
}
// 游戏时间
if (data.gameTime) {
Object.assign(gameStore.gameTime, data.gameTime)
}
// 活动任务
if (data.activeTasks) {
gameStore.activeTasks = JSON.parse(JSON.stringify(data.activeTasks))
}
// 负面状态
if (data.negativeStatus) {
gameStore.negativeStatus = JSON.parse(JSON.stringify(data.negativeStatus))
}
// 市场价格
if (data.marketPrices) {
gameStore.marketPrices = JSON.parse(JSON.stringify(data.marketPrices))
}
// 定时事件
if (data.scheduledEvents) {
gameStore.scheduledEvents = JSON.parse(JSON.stringify(data.scheduledEvents))
}
// 清除不持久化的状态
gameStore.logs = []
gameStore.inCombat = false
gameStore.combatState = null
gameStore.currentEvent = null
}
/**
* 重置玩家数据
* @param {Object} playerStore - 玩家Store
*/
function resetPlayerData(playerStore) {
// 基础属性
playerStore.baseStats = {
strength: 10,
agility: 8,
dexterity: 8,
intuition: 10,
vitality: 10
}
// 当前资源
playerStore.currentStats = {
health: 100,
maxHealth: 100,
stamina: 100,
maxStamina: 100,
sanity: 100,
maxSanity: 100
}
// 等级
playerStore.level = {
current: 1,
exp: 0,
maxExp: 100
}
// 技能
playerStore.skills = {}
// 装备
playerStore.equipment = {
weapon: null,
armor: null,
shield: null,
accessory: null
}
// 背包
playerStore.inventory = []
// 货币
playerStore.currency = { copper: 0 }
// 位置
playerStore.currentLocation = 'camp'
// 标志
playerStore.flags = {}
// 清空额外数据
resetSkillSystem(playerStore)
delete playerStore.equipmentStats
delete playerStore.killCount
}
/**
* 重置游戏数据
* @param {Object} gameStore - 游戏Store
*/
function resetGameData(gameStore) {
gameStore.currentTab = 'status'
gameStore.drawerState = {
inventory: false,
event: false
}
gameStore.logs = []
gameStore.gameTime = {
day: 1,
hour: 8,
minute: 0,
totalMinutes: 480
}
gameStore.inCombat = false
gameStore.combatState = null
gameStore.activeTasks = []
gameStore.negativeStatus = []
gameStore.marketPrices = {
lastRefreshDay: 1,
prices: {}
}
gameStore.currentEvent = null
gameStore.scheduledEvents = []
}
/**
* 版本迁移
* @param {Number} fromVersion - 源版本
* @param {Number} toVersion - 目标版本
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
*/
function migrateSaveData(fromVersion, toVersion, gameStore, playerStore) {
console.log(`Migrating save from v${fromVersion} to v${toVersion}`)
// 版本1暂无迁移需求
// 未来版本可以在这里添加迁移逻辑
console.log('Save migration completed')
}
/**
* 检查是否存在存档
* @returns {boolean}
*/
export function hasSaveData() {
try {
const saveData = uni.getStorageSync(SAVE_KEY)
return !!saveData
} catch (error) {
return false
}
}
/**
* 获取存档信息
* @returns {Object|null} 存档信息
*/
export function getSaveInfo() {
try {
const saveDataRaw = uni.getStorageSync(SAVE_KEY)
if (!saveDataRaw) return null
const saveData = JSON.parse(saveDataRaw)
return {
version: saveData.version,
timestamp: saveData.timestamp,
playTime: saveData.timestamp,
gameTime: saveData.game?.gameTime
}
} catch (error) {
return null
}
}
/**
* 导出存档(字符串)
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @returns {string|null} 存档字符串
*/
export function exportSave(gameStore, playerStore) {
try {
const saveData = {
version: SAVE_VERSION,
timestamp: Date.now(),
player: serializePlayerData(playerStore),
game: serializeGameData(gameStore)
}
return JSON.stringify(saveData)
} catch (error) {
console.error('Failed to export save:', error)
return null
}
}
/**
* 导入存档(从字符串)
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {string} saveString - 存档字符串
* @returns {boolean} 是否成功
*/
export function importSave(gameStore, playerStore, saveString) {
try {
const saveData = JSON.parse(saveString)
// 验证格式
if (!saveData.version || !saveData.player || !saveData.game) {
return false
}
// 应用存档
applySaveData(gameStore, playerStore, saveData)
// 保存到本地
uni.setStorageSync(SAVE_KEY, saveString)
return true
} catch (error) {
console.error('Failed to import save:', error)
return false
}
}

600
utils/taskSystem.js Normal file
View File

@@ -0,0 +1,600 @@
/**
* 任务系统 - 挂机任务管理、互斥检测
* Phase 6 核心系统实现
*/
import { SKILL_CONFIG } from '@/config/skills.js'
import { ITEM_CONFIG } from '@/config/items.js'
/**
* 任务类型枚举
*/
export const TASK_TYPES = {
READING: 'reading', // 阅读
RESTING: 'resting', // 休息
TRAINING: 'training', // 训练
WORKING: 'working', // 工作
COMBAT: 'combat', // 战斗
EXPLORE: 'explore', // 探索
PRAYING: 'praying' // 祈祷
}
/**
* 任务互斥规则
*/
const MUTEX_RULES = {
// 完全互斥(不能同时进行)
full: [
[TASK_TYPES.COMBAT, TASK_TYPES.TRAINING], // 战斗 vs 训练
[TASK_TYPES.COMBAT, TASK_TYPES.WORKING], // 战斗 vs 工作
[TASK_TYPES.TRAINING, TASK_TYPES.WORKING], // 训练 vs 工作
[TASK_TYPES.EXPLORE, TASK_TYPES.RESTING] // 探索 vs 休息
],
// 部分互斥(需要一心多用技能)
partial: [
[TASK_TYPES.READING, TASK_TYPES.COMBAT], // 阅读 vs 战斗
[TASK_TYPES.READING, TASK_TYPES.TRAINING], // 阅读 vs 训练
[TASK_TYPES.READING, TASK_TYPES.WORKING] // 阅读 vs 工作
]
}
/**
* 开始任务
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {String} taskType - 任务类型
* @param {Object} taskData - 任务数据
* @returns {Object} { success: boolean, message: string, taskId: string|null }
*/
export function startTask(gameStore, playerStore, taskType, taskData = {}) {
// 检查是否可以开始任务
const canStart = canStartTask(gameStore, playerStore, taskType)
if (!canStart.canStart) {
return { success: false, message: canStart.reason }
}
// 检查任务特定条件
const taskCheck = checkTaskConditions(playerStore, taskType, taskData)
if (!taskCheck.canStart) {
return { success: false, message: taskCheck.reason }
}
// 创建任务
const task = {
id: Date.now(),
type: taskType,
data: taskData,
startTime: Date.now(),
progress: 0,
totalDuration: taskData.duration || 0,
lastTickTime: Date.now()
}
gameStore.activeTasks.push(task)
return {
success: true,
message: '任务已开始',
taskId: task.id
}
}
/**
* 检查是否可以开始任务
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {String} taskType - 任务类型
* @returns {Object} { canStart: boolean, reason: string }
*/
export function canStartTask(gameStore, playerStore, taskType) {
// 检查是否已有相同类型的任务
const existingTask = gameStore.activeTasks.find(t => t.type === taskType)
if (existingTask) {
return { canStart: false, reason: '已有相同类型的任务正在进行' }
}
// 检查互斥规则
for (const [type1, type2] of MUTEX_RULES.full) {
if (taskType === type1) {
const conflictTask = gameStore.activeTasks.find(t => t.type === type2)
if (conflictTask) {
return { canStart: false, reason: `${getTaskTypeName(type2)}任务冲突` }
}
}
if (taskType === type2) {
const conflictTask = gameStore.activeTasks.find(t => t.type === type1)
if (conflictTask) {
return { canStart: false, reason: `${getTaskTypeName(type1)}任务冲突` }
}
}
}
// 检查部分互斥(需要一心多用技能)
const multitaskingLevel = playerStore.skills.multitasking?.level || 0
const maxConcurrentTasks = 1 + Math.floor(multitaskingLevel / 5) // 每5级一心多用可多1个任务
const activePartialTasks = gameStore.activeTasks.filter(t =>
MUTEX_RULES.partial.some(pair => pair.includes(t.type))
).length
const isPartialConflict = MUTEX_RULES.partial.some(pair =>
pair.includes(taskType) && pair.some(type =>
gameStore.activeTasks.some(t => t.type === type)
)
)
if (isPartialConflict && activePartialTasks >= maxConcurrentTasks) {
return { canStart: false, reason: `需要一心多用技能Lv.${multitaskingLevel + 1}` }
}
return { canStart: true, reason: '' }
}
/**
* 检查任务特定条件
*/
function checkTaskConditions(playerStore, taskType, taskData) {
switch (taskType) {
case TASK_TYPES.READING:
// 需要书籍
if (!taskData.itemId) {
return { canStart: false, reason: '需要选择一本书' }
}
const bookConfig = ITEM_CONFIG[taskData.itemId]
if (!bookConfig || bookConfig.type !== 'book') {
return { canStart: false, reason: '这不是一本书' }
}
// 检查是否已拥有
const hasBook = playerStore.inventory.some(i => i.id === taskData.itemId)
if (!hasBook) {
return { canStart: false, reason: '你没有这本书' }
}
break
case TASK_TYPES.TRAINING:
// 需要耐力
if (playerStore.currentStats.stamina < 10) {
return { canStart: false, reason: '耐力不足' }
}
// 需要武器技能
if (!taskData.skillId) {
return { canStart: false, reason: '需要选择训练技能' }
}
const skill = playerStore.skills[taskData.skillId]
if (!skill || !skill.unlocked) {
return { canStart: false, reason: '技能未解锁' }
}
break
case TASK_TYPES.RESTING:
// 只能在安全区休息
const safeZones = ['camp', 'market', 'blackmarket']
if (!safeZones.includes(playerStore.currentLocation)) {
return { canStart: false, reason: '只能在安全区休息' }
}
break
case TASK_TYPES.PRAYING:
// 需要圣经
const hasBible = playerStore.inventory.some(i => i.id === 'bible')
if (!hasBible) {
return { canStart: false, reason: '需要圣经' }
}
break
}
return { canStart: true, reason: '' }
}
/**
* 结束任务
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {String|Number} taskId - 任务ID
* @param {Boolean} completed - 是否完成
* @returns {Object} { success: boolean, rewards: Object }
*/
export function endTask(gameStore, playerStore, taskId, completed = false) {
const taskIndex = gameStore.activeTasks.findIndex(t => t.id === taskId)
if (taskIndex === -1) {
return { success: false, message: '任务不存在' }
}
const task = gameStore.activeTasks[taskIndex]
const rewards = {}
// 如果完成,给予奖励
if (completed) {
const taskRewards = getTaskRewards(playerStore, task)
Object.assign(rewards, taskRewards)
applyTaskRewards(playerStore, task, taskRewards)
}
// 移除任务
gameStore.activeTasks.splice(taskIndex, 1)
return {
success: true,
message: completed ? '任务完成' : '任务已取消',
rewards
}
}
/**
* 处理任务tick每秒调用
* @param {Object} gameStore - 游戏Store
* @param {Object} playerStore - 玩家Store
* @param {Number} deltaTime - 时间增量(毫秒)
* @returns {Array} 完成的任务列表
*/
export function processTaskTick(gameStore, playerStore, deltaTime = 1000) {
const completedTasks = []
for (const task of gameStore.activeTasks) {
const result = processSingleTask(gameStore, playerStore, task, deltaTime)
if (result.completed) {
completedTasks.push({ task, rewards: result.rewards })
}
}
// 移除已完成的任务
for (const { task } of completedTasks) {
const index = gameStore.activeTasks.findIndex(t => t.id === task.id)
if (index !== -1) {
gameStore.activeTasks.splice(index, 1)
}
}
return completedTasks
}
/**
* 处理单个任务
*/
function processSingleTask(gameStore, playerStore, task, deltaTime) {
const now = Date.now()
const elapsedSeconds = (now - task.lastTickTime) / 1000
task.lastTickTime = now
task.progress += elapsedSeconds
switch (task.type) {
case TASK_TYPES.READING:
return processReadingTask(gameStore, playerStore, task, elapsedSeconds)
case TASK_TYPES.RESTING:
return processRestingTask(gameStore, playerStore, task, elapsedSeconds)
case TASK_TYPES.TRAINING:
return processTrainingTask(gameStore, playerStore, task, elapsedSeconds)
case TASK_TYPES.WORKING:
return processWorkingTask(gameStore, playerStore, task, elapsedSeconds)
case TASK_TYPES.PRAYING:
return processPrayingTask(gameStore, playerStore, task, elapsedSeconds)
default:
return { completed: false }
}
}
/**
* 处理阅读任务
*/
function processReadingTask(gameStore, playerStore, task, elapsedSeconds) {
const bookConfig = ITEM_CONFIG[task.data.itemId]
if (!bookConfig) {
return { completed: true, error: '书籍不存在' }
}
const readingTime = bookConfig.readingTime || 60
// 检查环境惩罚
const location = playerStore.currentLocation
let timeMultiplier = 1.0
// 黑暗区域阅读效率-50%
if (location === 'basement') {
const darkPenaltyReduce = playerStore.globalBonus?.darkPenaltyReduce || 0
timeMultiplier = 0.5 + (darkPenaltyReduce / 100) * 0.5
}
// 计算有效进度
const effectiveProgress = task.progress * timeMultiplier
if (effectiveProgress >= readingTime) {
// 阅读完成
const rewards = {}
// 给予技能经验
if (bookConfig.expReward) {
for (const [skillId, exp] of Object.entries(bookConfig.expReward)) {
rewards[skillId] = exp
}
}
// 完成奖励
if (bookConfig.completionBonus) {
rewards.completionBonus = bookConfig.completionBonus
}
// 添加日志
if (gameStore.addLog) {
gameStore.addLog(`读完了《${bookConfig.name}`, 'reward')
}
return { completed: true, rewards }
}
// 阅读进行中,给予少量经验
if (bookConfig.expReward) {
for (const [skillId, expPerSecond] of Object.entries(bookConfig.expReward)) {
const expPerTick = (expPerSecond / readingTime) * elapsedSeconds * timeMultiplier
// 累积经验,到完成时一次性给予
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp[skillId]) {
task.accumulatedExp[skillId] = 0
}
task.accumulatedExp[skillId] += expPerTick
}
}
return { completed: false }
}
/**
* 处理休息任务
*/
function processRestingTask(gameStore, playerStore, task, elapsedSeconds) {
// 恢复耐力和生命值
const staminaRecovery = 5 * elapsedSeconds // 每秒恢复5点
const healthRecovery = 2 * elapsedSeconds // 每秒恢复2点
const oldStamina = playerStore.currentStats.stamina
const oldHealth = playerStore.currentStats.health
playerStore.currentStats.stamina = Math.min(
playerStore.currentStats.maxStamina,
playerStore.currentStats.stamina + staminaRecovery
)
playerStore.currentStats.health = Math.min(
playerStore.currentStats.maxHealth,
playerStore.currentStats.health + healthRecovery
)
// 检查是否完成
if (task.data.duration && task.progress >= task.data.duration) {
return { completed: true, rewards: {} }
}
// 满状态自动完成
if (playerStore.currentStats.stamina >= playerStore.currentStats.maxStamina &&
playerStore.currentStats.health >= playerStore.currentStats.maxHealth) {
return { completed: true, rewards: {} }
}
return { completed: false }
}
/**
* 处理训练任务
*/
function processTrainingTask(gameStore, playerStore, task, elapsedSeconds) {
const skillId = task.data.skillId
const skillConfig = SKILL_CONFIG[skillId]
if (!skillConfig) {
return { completed: true, error: '技能不存在' }
}
// 消耗耐力
const staminaCost = 2 * elapsedSeconds
if (playerStore.currentStats.stamina < staminaCost) {
// 耐力不足,任务结束
return { completed: true, rewards: {} }
}
playerStore.currentStats.stamina -= staminaCost
// 给予技能经验
const expPerSecond = 1
const expGain = expPerSecond * elapsedSeconds
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp[skillId]) {
task.accumulatedExp[skillId] = 0
}
task.accumulatedExp[skillId] += expGain
// 检查是否完成
if (task.data.duration && task.progress >= task.data.duration) {
const rewards = { [skillId]: task.accumulatedExp[skillId] || 0 }
return { completed: true, rewards }
}
return { completed: false }
}
/**
* 处理工作任务
*/
function processWorkingTask(gameStore, playerStore, task, elapsedSeconds) {
// 消耗耐力
const staminaCost = 1.5 * elapsedSeconds
if (playerStore.currentStats.stamina < staminaCost) {
return { completed: true, rewards: {} }
}
playerStore.currentStats.stamina -= staminaCost
// 累积货币
if (!task.accumulatedCurrency) {
task.accumulatedCurrency = 0
}
task.accumulatedCurrency += 1 * elapsedSeconds // 每秒1铜币
// 给予相关技能经验(如果有)
if (task.data.skillId) {
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp[task.data.skillId]) {
task.accumulatedExp[task.data.skillId] = 0
}
task.accumulatedExp[task.data.skillId] += 0.5 * elapsedSeconds
}
// 检查是否完成
if (task.data.duration && task.progress >= task.data.duration) {
const rewards = {
currency: Math.floor(task.accumulatedCurrency || 0),
...task.accumulatedExp
}
return { completed: true, rewards }
}
return { completed: false }
}
/**
* 处理祈祷任务
*/
function processPrayingTask(gameStore, playerStore, task, elapsedSeconds) {
// 祈祷获得信仰经验
if (!task.accumulatedExp) {
task.accumulatedExp = {}
}
if (!task.accumulatedExp.faith) {
task.accumulatedExp.faith = 0
}
task.accumulatedExp.faith += 0.8 * elapsedSeconds
// 恢复精神
const sanityRecovery = 3 * elapsedSeconds
playerStore.currentStats.sanity = Math.min(
playerStore.currentStats.maxSanity,
playerStore.currentStats.sanity + sanityRecovery
)
// 检查是否完成
if (task.data.duration && task.progress >= task.data.duration) {
const rewards = { faith: task.accumulatedExp.faith || 0 }
return { completed: true, rewards }
}
return { completed: false }
}
/**
* 获取任务奖励
*/
function getTaskRewards(playerStore, task) {
const rewards = {}
if (task.accumulatedExp) {
for (const [skillId, exp] of Object.entries(task.accumulatedExp)) {
rewards[skillId] = Math.floor(exp)
}
}
if (task.accumulatedCurrency) {
rewards.currency = Math.floor(task.accumulatedCurrency)
}
return rewards
}
/**
* 应用任务奖励
*/
function applyTaskRewards(playerStore, task, rewards) {
// 应用技能经验
for (const [skillId, exp] of Object.entries(rewards)) {
if (skillId === 'currency') {
playerStore.currency.copper += exp
continue
}
if (skillId === 'faith') {
// 信仰技能经验
if (!playerStore.skills.faith) {
playerStore.skills.faith = { level: 0, exp: 0, unlocked: true }
}
playerStore.skills.faith.exp += exp
continue
}
// 普通技能经验
if (playerStore.skills[skillId]) {
playerStore.skills[skillId].exp += exp
}
}
}
/**
* 获取任务类型名称
*/
function getTaskTypeName(taskType) {
const names = {
reading: '阅读',
resting: '休息',
training: '训练',
working: '工作',
combat: '战斗',
explore: '探索',
praying: '祈祷'
}
return names[taskType] || taskType
}
/**
* 获取任务进度信息
* @param {Object} task - 任务对象
* @returns {Object} 进度信息
*/
export function getTaskProgress(task) {
const progress = {
current: task.progress,
total: task.totalDuration || 0,
percentage: task.totalDuration > 0 ? Math.min(100, (task.progress / task.totalDuration) * 100) : 0
}
if (task.type === TASK_TYPES.READING) {
const bookConfig = ITEM_CONFIG[task.data.itemId]
if (bookConfig) {
progress.total = bookConfig.readingTime || 60
progress.percentage = Math.min(100, (task.progress / progress.total) * 100)
}
}
return progress
}
/**
* 获取活动中的任务列表
* @param {Object} gameStore - 游戏Store
* @returns {Array} 任务列表
*/
export function getActiveTasks(gameStore) {
return gameStore.activeTasks.map(task => ({
...task,
progress: getTaskProgress(task),
typeName: getTaskTypeName(task.type)
}))
}
/**
* 取消所有任务
* @param {Object} gameStore - 游戏Store
* @returns {Number} 取消的任务数量
*/
export function cancelAllTasks(gameStore) {
const count = gameStore.activeTasks.length
gameStore.activeTasks = []
return count
}