feat: 完善数学模型和品质系统

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-25 14:54:20 +08:00
parent cef974d94f
commit 5d4371ba1f
9 changed files with 2459 additions and 82 deletions

View File

@@ -1,4 +1,4 @@
// 游戏数值常量
// 游戏数值常量 - 与数学模型文档保持一致
export const GAME_CONSTANTS = {
// 时间流速现实1秒 = 游戏时间5分钟
TIME_SCALE: 5,
@@ -14,13 +14,49 @@ export const GAME_CONSTANTS = {
// 经验倍率
EXP_PARTIAL_SUCCESS: 0.5, // 部分成功时经验比例
// 品质等级
// 品质等级 - 与数学模型文档一致
QUALITY_LEVELS: {
1: { name: '垃圾', color: '#808080', range: [0, 49], multiplier: 1.0 },
1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 },
2: { name: '普通', color: '#ffffff', range: [50, 99], multiplier: 1.0 },
3: { name: '优秀', color: '#4ade80', range: [100, 129], multiplier: 1.1 },
4: { name: '稀有', color: '#60a5fa', range: [130, 159], multiplier: 1.3 },
5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 1.6 },
6: { name: '传说', color: '#f97316', range: [200, 250], multiplier: 2.0 }
3: { name: '优秀', color: '#22c55e', range: [100, 139], multiplier: 1.2 },
4: { name: '稀有', color: '#3b82f6', range: [140, 169], multiplier: 1.5 },
5: { name: '史诗', color: '#a855f7', range: [170, 199], multiplier: 2.0 },
6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 }
},
// 属性缩放系数
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%
}
}

498
config/formulas.js Normal file
View File

@@ -0,0 +1,498 @@
/**
* 游戏数值模型配置
*
* 本文件定义了游戏中所有数值计算的核心公式
* 包括:角色属性、装备属性、技能效果、怪物属性等
*/
// ==================== 核心常量 ====================
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)
}