Files
text-adventure-game/config/formulas.js
Claude 5d4371ba1f feat: 完善数学模型和品质系统
- 修复品质等级范围重叠问题(优秀/稀有无重叠)
- 统一 formulas.js 与 constants.js 的品质判定
- 修复经验公式与游戏实际逻辑不一致
- 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10%
- 添加数学模型文档和README
- 添加数值验证脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:54:20 +08:00

499 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 游戏数值模型配置
*
* 本文件定义了游戏中所有数值计算的核心公式
* 包括:角色属性、装备属性、技能效果、怪物属性等
*/
// ==================== 核心常量 ====================
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)
}