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