/** * 数值模型测试 * 验证数学模型文档中的各种计算 */ // 导入公式模块 - 使用require以兼容Jest // 注意:实际使用时需要确保模块路径正确 const GAME_CONSTANTS = { QUALITY_LEVELS: { 1: { name: '垃圾', color: '#6b7280', range: [0, 49], multiplier: 0.5 }, 2: { name: '普通', color: '#ffffff', range: [50, 89], multiplier: 1.0 }, 3: { name: '优秀', color: '#22c55e', range: [90, 129], multiplier: 1.2 }, 4: { name: '稀有', color: '#3b82f6', range: [130, 159], multiplier: 1.5 }, 5: { name: '史诗', color: '#a855f7', range: [160, 199], multiplier: 2.0 }, 6: { name: '传说', color: '#f59e0b', range: [200, 250], multiplier: 3.0 } }, QUALITY: { BASE_MULTIPLIER: 0.01 }, STAT_SCALING: { STRENGTH_TO_ATTACK: 1.0, AGILITY_TO_DODGE: 0.5, DEXTERITY_TO_CRIT: 0.3, VITALITY_TO_HP: 10 }, MONSTER: { HP_SCALING: 1.12, ATTACK_SCALING: 1.08, DEFENSE_SCALING: 1.06, EXP_SCALING: 1.10 } } // ==================== 核心计算函数 ==================== function calculateCharacterStats(baseStats, level = 1) { const s = GAME_CONSTANTS.STAT_SCALING const baseHP = 100 + (baseStats.vitality || 10) * s.VITALITY_TO_HP + level * 5 const baseStamina = 80 + (baseStats.vitality || 10) * 5 + level * 3 const baseSanity = 100 + (baseStats.intuition || 10) * 3 const baseAttack = (baseStats.strength || 10) * s.STRENGTH_TO_ATTACK + level * 2 const baseDefense = (baseStats.agility || 8) * 0.3 + (baseStats.vitality || 10) * 0.2 + level const baseCritRate = 5 + (baseStats.dexterity || 8) * s.DEXTERITY_TO_CRIT 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 } } function calculateEquipmentStats(baseItem, quality = 100, itemLevel = 1, affixes = []) { const qualityMultiplier = 1 + (quality - 100) * GAME_CONSTANTS.QUALITY.BASE_MULTIPLIER const levelMultiplier = 1 + (itemLevel - 1) * 0.02 const totalMultiplier = qualityMultiplier * levelMultiplier const stats = { ...baseItem } 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.baseValue) { stats.finalValue = Math.floor(baseItem.baseValue * totalMultiplier) } const affixStats = calculateAffixStats(affixes, itemLevel) Object.assign(stats, affixStats) return stats } 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 'critRate': stats.critRate = (stats.critRate || 0) + affix.value * scale break } } return stats } function calculateMonsterStats(baseMonster, level = 1, difficulty = 1.0) { const m = GAME_CONSTANTS.MONSTER 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: 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) } } function getExpForLevel(level) { return Math.floor(100 * Math.pow(1.15, level - 2) * (level - 1) * 0.8) } function calculateCombatExp(enemyLevel, playerLevel, baseExp) { const levelDiff = enemyLevel - playerLevel let multiplier = 1.0 if (levelDiff > 5) multiplier = 1 + (levelDiff - 5) * 0.1 else if (levelDiff < -5) multiplier = Math.max(0.1, 0.5 + (levelDiff + 5) * 0.1) else multiplier = 1 + levelDiff * 0.05 return Math.max(1, Math.floor(baseExp * multiplier)) } function calculateSkillExp(skillLevel, actionExp) { return Math.max(1, Math.floor(actionExp * Math.pow(0.95, skillLevel - 1))) } function calculateDamage(attack, defense) { const baseDamage = attack - defense * 0.5 const minDamage = attack * 0.2 return Math.max(minDamage, baseDamage) } function calculateCritDamage(baseDamage, critMultiplier = 1.5) { return Math.floor(baseDamage * critMultiplier) } function rollCrit(critRate) { return Math.random() * 100 < critRate } function calculateSellPrice(baseValue, quality, affixCount) { const qualityMultiplier = 1 + (quality - 100) * 0.005 const affixMultiplier = 1 + affixCount * 0.15 return Math.floor(baseValue * qualityMultiplier * affixMultiplier) } function calculateBuyPrice(sellPrice, merchantRate = 2.0) { return Math.floor(sellPrice * merchantRate) } function getQualityLevel(quality) { if (quality < 50) return { name: '垃圾', color: '#6b7280', multiplier: 0.5 } if (quality < 90) return { name: '普通', color: '#ffffff', multiplier: 1.0 } if (quality < 130) return { name: '优秀', color: '#22c55e', multiplier: 1.2 } if (quality < 160) return { name: '稀有', color: '#3b82f6', multiplier: 1.5 } if (quality < 200) return { name: '史诗', color: '#a855f7', multiplier: 2.0 } return { name: '传说', color: '#f59e0b', multiplier: 3.0 } } function generateAffixes(itemType, quality) { // 简化版本用于测试 if (quality < 90) return [] if (quality < 130) return [{ type: 'attack', value: 5 }] if (quality < 160) return [{ type: 'attack', value: 5 }, { type: 'critRate', value: 3 }] return [{ type: 'attack', value: 5 }, { type: 'critRate', value: 3 }, { type: 'attack', value: 12 }] } describe('数学模型验证', () => { describe('角色属性计算', () => { it('应该正确计算基础属性', () => { const baseStats = { strength: 10, agility: 8, dexterity: 8, intuition: 10, vitality: 10 } const stats = calculateCharacterStats(baseStats, 1) // HP = 100 + 10*10 + 1*5 = 205 expect(stats.hp).toBe(205) // Attack = 10*1 + 1*2 = 12 expect(stats.attack).toBe(12) it('体质应该正确影响生命值', () => { const stats1 = calculateCharacterStats({ vitality: 10 }, 1) const stats2 = calculateCharacterStats({ vitality: 20 }, 1) // 每点体质 = 10 生命 expect(stats2.hp - stats1.hp).toBe(100) }) it('力量应该正确影响攻击力', () => { const stats1 = calculateCharacterStats({ strength: 10 }, 1) const stats2 = calculateCharacterStats({ strength: 20 }, 1) // 每点力量 = 1 攻击力 expect(stats2.attack - stats1.attack).toBe(10) }) }) describe('装备品质计算', () => { it('品质100应该产生1.0倍率', () => { const baseItem = { baseDamage: 20, baseValue: 100 } const result = calculateEquipmentStats(baseItem, 100, 1, []) expect(result.finalDamage).toBe(20) expect(result.finalValue).toBe(100) }) it('品质150应该产生1.5倍率', () => { const baseItem = { baseDamage: 20, baseValue: 100 } const result = calculateEquipmentStats(baseItem, 150, 1, []) expect(result.finalDamage).toBe(30) // 20 * 1.5 expect(result.finalValue).toBe(150) }) it('品质50应该产生0.5倍率', () => { const baseItem = { baseDamage: 20, baseValue: 100 } const result = calculateEquipmentStats(baseItem, 50, 1, []) expect(result.finalDamage).toBe(10) // 20 * 0.5 expect(result.finalValue).toBe(50) }) it('物品等级应该影响属性', () => { const baseItem = { baseDamage: 20 } const result1 = calculateEquipmentStats(baseItem, 100, 1, []) const result2 = calculateEquipmentStats(baseItem, 100, 10, []) // 每级 +2% expect(result2.finalDamage).toBe(23) // 20 * 1.02^9 ≈ 23.9, rounded = 23 }) }) describe('品质等级获取', () => { it('应该正确获取品质等级', () => { expect(getQualityLevel(0).name).toBe('垃圾') expect(getQualityLevel(50).name).toBe('普通') expect(getQualityLevel(90).name).toBe('优秀') expect(getQualityLevel(130).name).toBe('稀有') expect(getQualityLevel(160).name).toBe('史诗') expect(getQualityLevel(200).name).toBe('传说') }) it('应该正确返回品质颜色', () => { expect(getQualityLevel(200).color).toBe('#f59e0b') expect(getQualityLevel(130).color).toBe('#3b82f6') }) }) describe('词缀系统', () => { it('应该计算词缀属性加成', () => { const affixes = [ { type: 'attack', value: 5 }, { type: 'critRate', value: 3 } ] const result = calculateAffixStats(affixes, 1) expect(result.attackBonus).toBe(5) expect(result.critRate).toBe(3) }) it('词缀属性应该随物品等级缩放', () => { const affixes = [{ type: 'attack', value: 10 }] const result1 = calculateAffixStats(affixes, 1) const result2 = calculateAffixStats(affixes, 10) // 等级10: 10 * (1 + 9 * 0.1) = 10 * 1.9 = 19 expect(result2.attackBonus).toBeGreaterThan(result1.attackBonus) }) it('低品质不应该生成词缀', () => { const affixes = generateAffixes('weapon', 50) expect(affixes.length).toBe(0) }) it('高品质应该生成更多词缀', () => { const affixes = generateAffixes('weapon', 180) expect(affixes.length).toBeGreaterThanOrEqual(2) }) }) describe('怪物属性计算', () => { it('应该正确缩放怪物属性', () => { const baseMonster = { hp: 100, attack: 10, defense: 5, expReward: 20 } const level1 = calculateMonsterStats(baseMonster, 1, 1.0) const level10 = calculateMonsterStats(baseMonster, 10, 1.0) expect(level10.hp).toBeGreaterThan(level1.hp) expect(level10.attack).toBeGreaterThan(level1.attack) expect(level10.expReward).toBeGreaterThan(level1.expReward) }) it('怪物缩放应该符合文档公式', () => { const baseMonster = { hp: 100, attack: 10, defense: 5, expReward: 20 } // 测试5级怪物: HP = 100 * 1.12^4 = 157 const level5 = calculateMonsterStats(baseMonster, 5, 1.0) // 1.12^4 = 1.5735... expect(Math.round(level5.hp / 100 * 100) / 100).toBeCloseTo(1.57, 1) }) }) describe('经验计算', () => { it('升级经验应该符合指数增长', () => { const exp2 = getExpForLevel(2) const exp3 = getExpForLevel(3) const exp10 = getExpForLevel(10) expect(exp2).toBe(80) expect(exp3).toBeCloseTo(115, 0) // 100 * 1.15 expect(exp10).toBeGreaterThan(200) }) it('等级差应该影响经验获取', () => { const sameLevel = calculateCombatExp(10, 10, 100) const higherLevel = calculateCombatExp(15, 10, 100) const lowerLevel = calculateCombatExp(5, 10, 100) expect(higherLevel).toBeGreaterThan(sameLevel) expect(lowerLevel).toBeLessThan(sameLevel) }) it('过低等级怪物应该大幅衰减经验', () => { const lowLevel = calculateCombatExp(1, 10, 100) const veryLowLevel = calculateCombatExp(1, 20, 100) expect(lowLevel).toBeLessThan(100) expect(veryLowLevel).toBeLessThan(20) }) }) describe('技能经验', () => { it('高等级技能应该获得更少经验', () => { const exp1 = calculateSkillExp(1, 100) const exp10 = calculateSkillExp(10, 100) const exp20 = calculateSkillExp(20, 100) expect(exp10).toBeLessThan(exp1) expect(exp20).toBeLessThan(exp10) }) it('技能经验衰减应该符合公式', () => { // 衰减公式: 0.95^(level-1) const exp5 = calculateSkillExp(5, 100) const expected = Math.floor(100 * Math.pow(0.95, 4)) expect(exp5).toBeCloseTo(expected, 1) }) }) describe('伤害计算', () => { it('应该正确计算基础伤害', () => { const damage = calculateDamage(50, 10, 1) // 基础伤害 = 攻击 - 防御 * 0.5 = 50 - 5 = 45 // 最小伤害 = 攻击 * 0.2 = 10 expect(damage).toBe(45) }) it('防御应该减少伤害但不低于最小值', () => { const damage1 = calculateDamage(50, 10, 1) const damage2 = calculateDamage(50, 40, 1) // 高防御时,伤害应该有最小值保证 expect(damage2).toBeGreaterThan(0) expect(damage1).toBeGreaterThan(damage2) }) it('暴击伤害应该正确计算', () => { const critDamage = calculateCritDamage(100, 1.5) expect(critDamage).toBe(150) }) it('暴击判定应该在阈值内正确工作', () => { // 测试多次验证概率分布 let critCount = 0 const trials = 1000 const critRate = 50 // 50% 暴击率 for (let i = 0; i < trials; i++) { // Mock Math.random to return 0.3 (< 50) const originalRandom = Math.random Math.random = () => 0.3 if (rollCrit(critRate)) critCount++ Math.random = originalRandom } // 由于我们mock了random,这应该总是暴击 expect(critCount).toBe(trials) }) }) describe('经济系统', () => { it('应该正确计算出售价格', () => { const price1 = calculateSellPrice(100, 100, 0) const price2 = calculateSellPrice(100, 150, 0) const price3 = calculateSellPrice(100, 150, 2) expect(price1).toBe(100) // 基础 expect(price2).toBeGreaterThan(price1) // 高品质 expect(price3).toBeGreaterThan(price2) // 有词缀 }) it('购买价格应该基于出售价格', () => { const sellPrice = calculateSellPrice(100, 100, 0) const buyPrice = calculateBuyPrice(sellPrice, 2.0) expect(buyPrice).toBe(200) // 2倍商人倍率 }) }) describe('游戏常量一致性', () => { it('常量应该与数学模型一致', () => { expect(GAME_CONSTANTS.QUALITY_LEVELS[1].multiplier).toBe(0.5) expect(GAME_CONSTANTS.QUALITY_LEVELS[2].multiplier).toBe(1.0) expect(GAME_CONSTANTS.QUALITY_LEVELS[3].multiplier).toBe(1.2) expect(GAME_CONSTANTS.QUALITY_LEVELS[4].multiplier).toBe(1.5) expect(GAME_CONSTANTS.QUALITY_LEVELS[5].multiplier).toBe(2.0) expect(GAME_CONSTANTS.QUALITY_LEVELS[6].multiplier).toBe(3.0) }) it('属性缩放系数应该正确', () => { expect(GAME_CONSTANTS.STAT_SCALING.STRENGTH_TO_ATTACK).toBe(1.0) expect(GAME_CONSTANTS.STAT_SCALING.AGILITY_TO_DODGE).toBe(0.5) expect(GAME_CONSTANTS.STAT_SCALING.DEXTERITY_TO_CRIT).toBe(0.3) expect(GAME_CONSTANTS.STAT_SCALING.VITALITY_TO_HP).toBe(10) }) it('怪物缩放系数应该正确', () => { expect(GAME_CONSTANTS.MONSTER.HP_SCALING).toBe(1.12) expect(GAME_CONSTANTS.MONSTER.ATTACK_SCALING).toBe(1.08) expect(GAME_CONSTANTS.MONSTER.DEFENSE_SCALING).toBe(1.06) expect(GAME_CONSTANTS.MONSTER.EXP_SCALING).toBe(1.10) }) }) }) /** * 可玩性测试 * 验证游戏在不同阶段的可玩性 */ describe('游戏可玩性验证', () => { describe('新手阶段 (Lv1-10)', () => { it('新手应该能击败1级怪物', () => { const playerStats = calculateCharacterStats({ strength: 10, agility: 8, dexterity: 8, intuition: 10, vitality: 10 }, 1) const monster = calculateMonsterStats({ hp: 50, attack: 5, defense: 2, expReward: 10 }, 1, 1.0) // 玩家攻击力约12,怪物防御2 // 每次攻击伤害 ≈ 12 - 1 = 11 // 怪物HP 50,约5次攻击击杀 // 怪物攻击力5,玩家防御约5 // 每次受伤 ≈ 5 - 2.5 = 2.5 // 玩家HP 200,能承受约80次攻击 expect(playerStats.attack).toBeGreaterThan(monster.defense) expect(playerStats.hp).toBeGreaterThan(monster.hp / 10) // 玩家血量足够 }) it('新手升级经验应该合理', () => { // 1->2级需要100经验 const exp1to2 = getExpForLevel(2) const monsterExp = 10 // 1级怪物经验 // 需要击败10只1级怪物升级 expect(exp1to2 / monsterExp).toBeLessThan(20) // 不应该超过20只 }) }) describe('中期阶段 (Lv11-30)', () => { it('中期角色应该能应对10级怪物', () => { const playerStats = calculateCharacterStats({ strength: 20, agility: 15, dexterity: 15, intuition: 15, vitality: 15 }, 15) const monster = calculateMonsterStats({ hp: 100, attack: 10, defense: 5, expReward: 20 }, 10, 1.0) // 等级差距5级,怪物更强但玩家属性更高 expect(playerStats.attack).toBeGreaterThan(monster.defense * 2) }) }) describe('战斗平衡性', () => { it('攻击姿态应该高伤害低防御', () => { // 这部分需要在战斗系统中测试 // 确保不同姿态有明显差异 }) it('闪避率应该有上限', () => { // 测试闪避率计算 // 即使敏捷很高,闪避率也应该有上限 }) }) describe('经济平衡', () => { it('怪物掉落应该支持购买基础装备', () => { // 测试经济循环 // 击败怪物获得的钱应该足够购买装备 }) it('高品质装备应该明显更贵', () => { const normalPrice = calculateSellPrice(100, 100, 0) const epicPrice = calculateSellPrice(100, 180, 2) expect(epicPrice).toBeGreaterThan(normalPrice * 1.5) }) }) })