Files
text-adventure-game/utils/combatSystem.js

584 lines
16 KiB
JavaScript
Raw Normal View History

/**
* 战斗系统 - 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'
}