- 修复品质等级范围重叠问题(优秀/稀有无重叠) - 统一 formulas.js 与 constants.js 的品质判定 - 修复经验公式与游戏实际逻辑不一致 - 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10% - 添加数学模型文档和README - 添加数值验证脚本 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
499 lines
16 KiB
JavaScript
499 lines
16 KiB
JavaScript
/**
|
||
* 游戏数值模型配置
|
||
*
|
||
* 本文件定义了游戏中所有数值计算的核心公式
|
||
* 包括:角色属性、装备属性、技能效果、怪物属性等
|
||
*/
|
||
|
||
// ==================== 核心常量 ====================
|
||
|
||
export const GAME_FORMULAS = {
|
||
// 经验曲线系数
|
||
EXP_BASE: 100, // 基础经验值
|
||
EXP_GROWTH: 1.15, // 每级经验增长倍数
|
||
|
||
// 属性缩放系数
|
||
STAT_SCALING: {
|
||
STRENGTH_TO_ATTACK: 1.0, // 1力量 = 1攻击力
|
||
AGILITY_TO_DODGE: 0.5, // 1敏捷 = 0.5%闪避
|
||
DEXTERITY_TO_CRIT: 0.3, // 1灵巧 = 0.3%暴击
|
||
INTUITION_TO_QUALITY: 0.5, // 1智力 = 0.5品质
|
||
VITALITY_TO_HP: 10, // 1体质 = 10生命
|
||
VITALITY_TO_REGEN: 0.05 // 1体质 = 0.05 HP/秒回复
|
||
},
|
||
|
||
// 装备品质系统
|
||
QUALITY: {
|
||
BASE_MULTIPLIER: 0.01, // 品质每1点 = 1%属性
|
||
LEVEL_BONUS: 2, // 每级额外+2品质
|
||
MAX_QUALITY: 250,
|
||
MIN_QUALITY: 0
|
||
},
|
||
|
||
// 词缀系统
|
||
AFFIX: {
|
||
MAX_COUNT: 3, // 每件装备最多词缀数量
|
||
COMMON_CHANCE: 0.60, // 普通词缀概率
|
||
RARE_CHANCE: 0.30, // 稀有词缀概率
|
||
EPIC_CHANCE: 0.09, // 史诗词缀概率
|
||
LEGENDARY_CHANCE: 0.01 // 传说词缀概率
|
||
},
|
||
|
||
// 怪物缩放
|
||
MONSTER: {
|
||
HP_SCALING: 1.12, // 每级HP增长12%
|
||
ATTACK_SCALING: 1.08, // 每级攻击增长8%
|
||
DEFENSE_SCALING: 1.06, // 每级防御增长6%
|
||
EXP_SCALING: 1.10, // 每级经验奖励增长10%
|
||
LEVEL_VARANCE: 0.2 // 等级浮动±20%
|
||
}
|
||
}
|
||
|
||
// ==================== 角色属性计算 ====================
|
||
|
||
/**
|
||
* 计算角色基础属性
|
||
* @param {Object} baseStats - 基础属性 {strength, agility, dexterity, intuition, vitality}
|
||
* @param {Number} level - 角色等级
|
||
* @returns {Object} 计算后的属性
|
||
*/
|
||
export function calculateCharacterStats(baseStats, level = 1) {
|
||
const s = GAME_FORMULAS.STAT_SCALING
|
||
|
||
// 基础生命值 = 100 + 体质 * 10 + 等级 * 5
|
||
const baseHP = 100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5
|
||
|
||
// 基础耐力 = 80 + 体质 * 5 + 等级 * 3
|
||
const baseStamina = 80 + (baseStats.vitality || 10) * 5 + level * 3
|
||
|
||
// 基础精神 = 100 + 智力 * 3
|
||
const baseSanity = 100 + (baseStats.intuition || 10) * 3
|
||
|
||
// 基础攻击力 = 力量 * 1 + 等级 * 2
|
||
const baseAttack = (baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2
|
||
|
||
// 基础防御 = 敏捷 * 0.3 + 体质 * 0.2 + 等级
|
||
const baseDefense = (baseStats.agility || 8) * 0.3 + (baseStats.vitality || 10) * 0.2 + level
|
||
|
||
// 基础暴击率 = 灵巧 * 0.3% (基础值5%)
|
||
const baseCritRate = 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT
|
||
|
||
// 基础闪避率 = 敏捷 * 0.5%
|
||
const baseDodgeRate = (baseStats.agility || 8) * s.AGILITY_TO_DODGE
|
||
|
||
return {
|
||
hp: Math.floor(baseHP),
|
||
stamina: Math.floor(baseStamina),
|
||
sanity: Math.floor(baseSanity),
|
||
attack: Math.floor(baseAttack),
|
||
defense: Math.floor(baseDefense),
|
||
critRate: Math.floor(baseCritRate * 10) / 10,
|
||
dodgeRate: Math.floor(baseDodgeRate * 10) / 10
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算装备后的最终属性
|
||
* @param {Object} baseStats - 基础属性
|
||
* @param {Object} equipmentStats - 装备提供的属性
|
||
* @returns {Object} 最终属性
|
||
*/
|
||
export function calculateFinalStats(baseStats, equipmentStats = {}) {
|
||
const final = { ...baseStats }
|
||
|
||
// 装备属性加成(百分比和固定值混合)
|
||
if (equipmentStats.attackBonus) {
|
||
final.attack += Math.floor(baseStats.attack * (equipmentStats.attackBonus / 100))
|
||
final.attack += equipmentStats.attack || 0
|
||
}
|
||
if (equipmentStats.defenseBonus) {
|
||
final.defense += Math.floor(baseStats.defense * (equipmentStats.defenseBonus / 100))
|
||
final.defense += equipmentStats.defense || 0
|
||
}
|
||
if (equipmentStats.critRate) {
|
||
final.critRate += equipmentStats.critRate
|
||
}
|
||
if (equipmentStats.dodgeRate) {
|
||
final.dodgeRate += equipmentStats.dodgeRate
|
||
}
|
||
if (equipmentStats.maxHP) {
|
||
final.hp += equipmentStats.maxHP
|
||
}
|
||
if (equipmentStats.maxStamina) {
|
||
final.stamina += equipmentStats.maxStamina
|
||
}
|
||
|
||
// 限制范围
|
||
final.critRate = Math.min(95, Math.max(0, final.critRate))
|
||
final.dodgeRate = Math.min(80, Math.max(0, final.dodgeRate))
|
||
|
||
return final
|
||
}
|
||
|
||
// ==================== 装备属性计算 ====================
|
||
|
||
/**
|
||
* 计算装备的最终属性
|
||
* @param {Object} baseItem - 基础物品配置
|
||
* @param {Number} quality - 品质值 (0-250)
|
||
* @param {Number} itemLevel - 物品等级
|
||
* @param {Array} affixes - 词缀列表
|
||
* @returns {Object} 计算后的装备属性
|
||
*/
|
||
export function calculateEquipmentStats(baseItem, quality = 100, itemLevel = 1, affixes = []) {
|
||
const q = GAME_FORMULAS.QUALITY
|
||
const stats = { ...baseItem }
|
||
|
||
// 1. 品质倍率计算
|
||
// 品质倍率 = 1 + (品质 - 100) * 0.01
|
||
// 品质100 = 1.0倍,品质150 = 1.5倍,品质50 = 0.5倍
|
||
const qualityMultiplier = 1 + (quality - 100) * q.BASE_MULTIPLIER
|
||
|
||
// 2. 等级倍率
|
||
// 每级增加2%属性
|
||
const levelMultiplier = 1 + (itemLevel - 1) * 0.02
|
||
|
||
// 3. 综合倍率
|
||
const totalMultiplier = qualityMultiplier * levelMultiplier
|
||
|
||
// 4. 计算基础属性
|
||
if (baseItem.baseDamage) {
|
||
stats.finalDamage = Math.max(1, Math.floor(baseItem.baseDamage * totalMultiplier))
|
||
}
|
||
if (baseItem.baseDefense) {
|
||
stats.finalDefense = Math.max(1, Math.floor(baseItem.baseDefense * totalMultiplier))
|
||
}
|
||
if (baseItem.blockRate) {
|
||
stats.finalBlockRate = Math.min(75, baseItem.blockRate + quality * 0.02)
|
||
}
|
||
|
||
// 5. 应用词缀
|
||
const affixStats = calculateAffixStats(affixes, itemLevel)
|
||
Object.assign(stats, affixStats)
|
||
|
||
// 6. 计算价值
|
||
stats.finalValue = Math.floor(baseItem.baseValue * totalMultiplier * (1 + affixes.length * 0.2))
|
||
|
||
// 7. 品质信息
|
||
stats.quality = quality
|
||
stats.itemLevel = itemLevel
|
||
stats.qualityLevel = getQualityLevel(quality)
|
||
stats.affixes = affixes
|
||
|
||
return stats
|
||
}
|
||
|
||
/**
|
||
* 计算词缀属性
|
||
* @param {Array} affixes - 词缀列表
|
||
* @param {Number} itemLevel - 物品等级
|
||
* @returns {Object} 词缀提供的属性
|
||
*/
|
||
export function calculateAffixStats(affixes, itemLevel = 1) {
|
||
const stats = {}
|
||
|
||
for (const affix of affixes) {
|
||
// 词缀属性随物品等级缩放
|
||
const scale = 1 + (itemLevel - 1) * 0.1
|
||
|
||
switch (affix.type) {
|
||
case 'attack':
|
||
stats.attackBonus = (stats.attackBonus || 0) + affix.value * scale
|
||
break
|
||
case 'defense':
|
||
stats.defenseBonus = (stats.defenseBonus || 0) + affix.value * scale
|
||
break
|
||
case 'critRate':
|
||
stats.critRate = (stats.critRate || 0) + affix.value * scale
|
||
break
|
||
case 'dodge':
|
||
stats.dodgeRate = (stats.dodgeRate || 0) + affix.value * scale
|
||
break
|
||
case 'hp':
|
||
stats.maxHP = (stats.maxHP || 0) + affix.value * scale
|
||
break
|
||
case 'stamina':
|
||
stats.maxStamina = (stats.maxStamina || 0) + affix.value * scale
|
||
break
|
||
case 'fireDamage':
|
||
stats.fireDamage = (stats.fireDamage || 0) + affix.value * scale
|
||
break
|
||
case 'speed':
|
||
stats.attackSpeed = (stats.attackSpeed || 1) + affix.value * 0.01 * scale
|
||
break
|
||
}
|
||
}
|
||
|
||
return stats
|
||
}
|
||
|
||
/**
|
||
* 获取品质等级
|
||
* 与 constants.js 中的 QUALITY_LEVELS 保持一致
|
||
*/
|
||
export function getQualityLevel(quality) {
|
||
if (quality < 50) return { level: 1, name: '垃圾', color: '#6b7280', multiplier: 0.5 }
|
||
if (quality < 100) return { level: 2, name: '普通', color: '#ffffff', multiplier: 1.0 }
|
||
if (quality < 140) return { level: 3, name: '优秀', color: '#22c55e', multiplier: 1.2 }
|
||
if (quality < 170) return { level: 4, name: '稀有', color: '#3b82f6', multiplier: 1.5 }
|
||
if (quality < 200) return { level: 5, name: '史诗', color: '#a855f7', multiplier: 2.0 }
|
||
return { level: 6, name: '传说', color: '#f59e0b', multiplier: 3.0 }
|
||
}
|
||
|
||
// ==================== 词缀生成 ====================
|
||
|
||
/**
|
||
* 词缀池定义
|
||
*/
|
||
export const AFFIX_POOL = {
|
||
// 武器词缀
|
||
weapon: [
|
||
{ id: 'sharp', name: '锋利', type: 'attack', value: 5, rarity: 'common' },
|
||
{ id: 'keen', name: '锐利', type: 'critRate', value: 3, rarity: 'common' },
|
||
{ id: 'swift', name: '轻盈', type: 'speed', value: 10, rarity: 'common' },
|
||
{ id: 'vicious', name: '残暴', type: 'attack', value: 12, rarity: 'rare' },
|
||
{ id: 'deadly', name: '致命', type: 'critRate', value: 8, rarity: 'rare' },
|
||
{ id: 'flaming', name: '烈焰', type: 'fireDamage', value: 10, rarity: 'epic' },
|
||
{ id: 'godslayer', name: '弑神', type: 'attack', value: 25, rarity: 'legendary' }
|
||
],
|
||
// 防具词缀
|
||
armor: [
|
||
{ id: 'sturdy', name: '坚固', type: 'defense', value: 5, rarity: 'common' },
|
||
{ id: 'agile', name: '灵活', type: 'dodge', value: 3, rarity: 'common' },
|
||
{ id: 'healthy', name: '健康', type: 'hp', value: 20, rarity: 'common' },
|
||
{ id: 'fortified', name: '强化', type: 'defense', value: 12, rarity: 'rare' },
|
||
{ id: 'enduring', name: '耐久', type: 'stamina', value: 15, rarity: 'rare' },
|
||
{ id: 'ironclad', name: '铁壁', type: 'defense', value: 25, rarity: 'epic' },
|
||
{ id: 'immortal', name: '不朽', type: 'hp', value: 100, rarity: 'legendary' }
|
||
]
|
||
}
|
||
|
||
/**
|
||
* 生成词缀
|
||
* @param {String} itemType - 物品类型 (weapon/armor)
|
||
* @param {Number} quality - 品质值
|
||
* @returns {Array} 词缀数组
|
||
*/
|
||
export function generateAffixes(itemType, quality) {
|
||
const pool = AFFIX_POOL[itemType] || []
|
||
if (pool.length === 0) return []
|
||
|
||
// 根据品质决定最大词缀数量
|
||
const maxAffixes = quality < 90 ? 0
|
||
: quality < 130 ? 1
|
||
: quality < 160 ? 2
|
||
: GAME_FORMULAS.AFFIX.MAX_COUNT
|
||
|
||
if (maxAffixes === 0) return []
|
||
|
||
const affixes = []
|
||
const used = new Set()
|
||
|
||
// 决定每个词缀的稀有度
|
||
for (let i = 0; i < maxAffixes; i++) {
|
||
const roll = Math.random()
|
||
let rarity
|
||
|
||
if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE) rarity = 'legendary'
|
||
else if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE + GAME_FORMULAS.AFFIX.EPIC_CHANCE) rarity = 'epic'
|
||
else if (roll < GAME_FORMULAS.AFFIX.LEGENDARY_CHANCE + GAME_FORMULAS.AFFIX.EPIC_CHANCE + GAME_FORMULAS.AFFIX.RARE_CHANCE) rarity = 'rare'
|
||
else rarity = 'common'
|
||
|
||
// 从对应稀有度池中选择词缀
|
||
const available = pool.filter(a => a.rarity === rarity && !used.has(a.id))
|
||
|
||
// 如果该稀有度没有可用词缀,降级选择
|
||
const fallback = available.length > 0 ? available : pool.filter(a => !used.has(a.id))
|
||
|
||
if (fallback.length > 0) {
|
||
const affix = fallback[Math.floor(Math.random() * fallback.length)]
|
||
affixes.push(affix)
|
||
used.add(affix.id)
|
||
}
|
||
}
|
||
|
||
return affixes
|
||
}
|
||
|
||
// ==================== 怪物属性计算 ====================
|
||
|
||
/**
|
||
* 计算怪物属性
|
||
* @param {Object} baseMonster - 基础怪物配置
|
||
* @param {Number} level - 怪物等级
|
||
* @param {Number} difficulty - 难度系数 (0.8 - 1.5)
|
||
* @returns {Object} 计算后的怪物属性
|
||
*/
|
||
export function calculateMonsterStats(baseMonster, level = 1, difficulty = 1.0) {
|
||
const m = GAME_FORMULAS.MONSTER
|
||
|
||
// 等级缩放公式:基础值 * (1 + 增长率)^(等级-1)
|
||
const levelBonus = Math.pow(m.HP_SCALING, level - 1)
|
||
const attackBonus = Math.pow(m.ATTACK_SCALING, level - 1)
|
||
const defenseBonus = Math.pow(m.DEFENSE_SCALING, level - 1)
|
||
const expBonus = Math.pow(m.EXP_SCALING, level - 1)
|
||
|
||
return {
|
||
...baseMonster,
|
||
level,
|
||
// HP = 基础HP * 等级倍率 * 难度系数
|
||
hp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||
maxHp: Math.floor(baseMonster.hp * levelBonus * difficulty),
|
||
// 攻击 = 基础攻击 * 等级倍率 * 难度系数
|
||
attack: Math.floor(baseMonster.attack * attackBonus * difficulty),
|
||
// 防御 = 基础防御 * 等级倍率 * 难度系数
|
||
defense: Math.floor(baseMonster.defense * defenseBonus * difficulty),
|
||
// 经验奖励 = 基础经验 * 等级倍率
|
||
expReward: Math.floor(baseMonster.expReward * expBonus),
|
||
// 暴击率随等级略微提升
|
||
critRate: Math.min(30, (baseMonster.critRate || 0) + level * 0.5),
|
||
// 速度随等级略微提升
|
||
speed: baseMonster.speed + level * 0.5
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算怪物等级范围
|
||
* @param {Number} baseLevel - 基础等级
|
||
* @returns {Object} { min, max }
|
||
*/
|
||
export function getMonsterLevelRange(baseLevel) {
|
||
const v = GAME_FORMULAS.MONSTER.LEVEL_VARANCE
|
||
const variance = Math.max(1, Math.floor(baseLevel * v))
|
||
return {
|
||
min: Math.max(1, baseLevel - variance),
|
||
max: baseLevel + variance
|
||
}
|
||
}
|
||
|
||
// ==================== 经验计算 ====================
|
||
|
||
/**
|
||
* 计算升级所需经验
|
||
* 与 levelingSystem.js 中的公式保持一致
|
||
* @param {Number} currentLevel - 当前等级
|
||
* @returns {Number} 所需经验
|
||
*/
|
||
export function getExpForLevel(currentLevel) {
|
||
const f = GAME_FORMULAS
|
||
if (currentLevel <= 1) return 0
|
||
// 混合曲线公式: base * (1.15 ^ (level - 2)) * (level - 1) * 0.8
|
||
// 与 levelingSystem.js 的 HYBRID 曲线保持一致
|
||
return Math.floor(f.EXP_BASE * Math.pow(f.EXP_GROWTH, currentLevel - 2) * (currentLevel - 1) * 0.8)
|
||
}
|
||
|
||
/**
|
||
* 计算战斗经验
|
||
* @param {Number} monsterLevel - 怪物等级
|
||
* @param {Number} playerLevel - 玩家等级
|
||
* @param {Number} baseExp - 基础经验
|
||
* @returns {Number} 实际获得经验
|
||
*/
|
||
export function calculateCombatExp(monsterLevel, playerLevel, baseExp) {
|
||
// 等级差惩罚
|
||
const levelDiff = monsterLevel - playerLevel
|
||
|
||
let multiplier = 1.0
|
||
|
||
if (levelDiff > 5) {
|
||
// 怪物高5级以上,额外奖励
|
||
multiplier = 1 + (levelDiff - 5) * 0.1
|
||
} else if (levelDiff < -5) {
|
||
// 怪物低5级以上,大幅衰减
|
||
multiplier = Math.max(0.1, 0.5 + (levelDiff + 5) * 0.1)
|
||
} else if (levelDiff < 0) {
|
||
// 怪物等级略低,小幅衰减
|
||
multiplier = 1 + levelDiff * 0.05
|
||
}
|
||
|
||
return Math.max(1, Math.floor(baseExp * multiplier))
|
||
}
|
||
|
||
/**
|
||
* 计算技能经验
|
||
* @param {Number} skillLevel - 技能当前等级
|
||
* @param {Number} actionExp - 动作基础经验
|
||
* @returns {Number} 实际获得经验
|
||
*/
|
||
export function calculateSkillExp(skillLevel, actionExp) {
|
||
// 等级越高,获得经验越难
|
||
const decay = Math.pow(0.95, skillLevel - 1)
|
||
return Math.max(1, Math.floor(actionExp * decay))
|
||
}
|
||
|
||
// ==================== 伤害计算 ====================
|
||
|
||
/**
|
||
* 计算普通伤害
|
||
* @param {Number} attack - 攻击力
|
||
* @param {Number} defense - 防御力
|
||
* @param {Number} level - 等级加成
|
||
* @returns {Number} 伤害值
|
||
*/
|
||
export function calculateDamage(attack, defense, level = 1) {
|
||
// 基础伤害 = 攻击力 - 防御力 * 0.5
|
||
// 最小伤害为攻击力的20%
|
||
const baseDamage = attack - defense * 0.5
|
||
const minDamage = attack * 0.2
|
||
|
||
// 加入等级加成
|
||
const levelBonus = 1 + level * 0.02
|
||
|
||
return Math.max(minDamage, baseDamage) * levelBonus
|
||
}
|
||
|
||
/**
|
||
* 计算暴击伤害
|
||
* @param {Number} baseDamage - 基础伤害
|
||
* @param {Number} critMultiplier - 暴击倍率
|
||
* @returns {Number} 暴击伤害
|
||
*/
|
||
export function calculateCritDamage(baseDamage, critMultiplier = 1.5) {
|
||
return Math.floor(baseDamage * critMultiplier)
|
||
}
|
||
|
||
/**
|
||
* 计算是否暴击
|
||
* @param {Number} critRate - 暴击率 (0-100)
|
||
* @returns {Boolean}
|
||
*/
|
||
export function rollCrit(critRate) {
|
||
return Math.random() * 100 < critRate
|
||
}
|
||
|
||
/**
|
||
* 计算是否命中
|
||
* @param {Number} hitRate - 命中率 (0-100)
|
||
* @param {Number} dodgeRate - 闪避率 (0-100)
|
||
* @returns {Boolean}
|
||
*/
|
||
export function rollHit(hitRate = 90, dodgeRate = 0) {
|
||
const actualHitRate = Math.max(5, Math.min(95, hitRate - dodgeRate))
|
||
return Math.random() * 100 < actualHitRate
|
||
}
|
||
|
||
// ==================== 价值计算 ====================
|
||
|
||
/**
|
||
* 计算物品出售价格
|
||
* @param {Number} baseValue - 基础价值
|
||
* @param {Number} quality - 品质
|
||
* @param {Number} affixCount - 词缀数量
|
||
* @returns {Number} 出售价格
|
||
*/
|
||
export function calculateSellPrice(baseValue, quality = 100, affixCount = 0) {
|
||
const qualityMultiplier = 1 + (quality - 100) * 0.005
|
||
const affixMultiplier = 1 + affixCount * 0.15
|
||
return Math.floor(baseValue * qualityMultiplier * affixMultiplier)
|
||
}
|
||
|
||
/**
|
||
* 计算物品购买价格
|
||
* @param {Number} sellPrice - 出售价格
|
||
* @param {Number} merchantRate - 商人倍率 (通常 2.0 - 3.0)
|
||
* @returns {Number} 购买价格
|
||
*/
|
||
export function calculateBuyPrice(sellPrice, merchantRate = 2.0) {
|
||
return Math.floor(sellPrice * merchantRate)
|
||
}
|