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>
584 lines
16 KiB
JavaScript
584 lines
16 KiB
JavaScript
/**
|
||
* 战斗系统 - 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'
|
||
}
|